1
This commit is contained in:
@@ -79,12 +79,16 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
|
|
||||||
//设置登录信息l
|
//设置登录信息l
|
||||||
await userStore.setToken(loginRes.accessToken);
|
await userStore.setToken(loginRes.accessToken);
|
||||||
await userStore.asyncUserInfo();
|
await userStore.setUserInfo();
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
if (userStore.userInfo?.accountType == 1) {
|
if (userStore.userInfo?.accountType == 1) {
|
||||||
context.go(RoutePaths.sHome);
|
context.go(RoutePaths.sHome);
|
||||||
} else {
|
} else if(userStore.userInfo?.accountType == 2){
|
||||||
context.go(RoutePaths.sHome);
|
context.go(RoutePaths.tHome);
|
||||||
|
}else{
|
||||||
|
EasyLoading.showError("账号类型错误");
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:app/providers/user_store.dart';
|
import 'package:app/providers/user_store.dart';
|
||||||
import 'package:app/router/route_paths.dart';
|
import 'package:app/router/route_paths.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
@@ -28,15 +29,15 @@ class _SplashPageState extends State<SplashPage> {
|
|||||||
context.go(RoutePaths.login);
|
context.go(RoutePaths.login);
|
||||||
} else {
|
} else {
|
||||||
UserStore userStore = context.read<UserStore>();
|
UserStore userStore = context.read<UserStore>();
|
||||||
userStore.setUserInfo();
|
await userStore.init();
|
||||||
//去学生主页
|
//去学生主页
|
||||||
if (userStore.userInfo?.accountType == 1) {
|
if (userStore.userInfo?.accountType == 1) {
|
||||||
context.go(RoutePaths.sHome);
|
context.go(RoutePaths.sHome);
|
||||||
} else {
|
} else if(userStore.userInfo?.accountType == 2){
|
||||||
context.go(RoutePaths.tHome);
|
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/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/api/room_api.dart';
|
||||||
import 'package:app/request/dto/room/room_type_dto.dart';
|
import 'package:app/request/dto/room/room_type_dto.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import 'today/s_today_card.dart';
|
import 'today/s_today_card.dart';
|
||||||
import 'widgets/user_header.dart';
|
import 'widgets/user_header.dart';
|
||||||
|
|
||||||
class SHomePage extends StatefulWidget {
|
class SHomePage extends StatelessWidget {
|
||||||
const SHomePage({super.key});
|
const SHomePage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<SHomePage> createState() => _SHomePageState();
|
Widget build(BuildContext context) {
|
||||||
|
return ChangeNotifierProvider(
|
||||||
|
create: (_) => SHomeVm(),
|
||||||
|
child: _HomeView(),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SHomePageState extends State<SHomePage> {
|
class _HomeView extends StatelessWidget {
|
||||||
|
const _HomeView({super.key});
|
||||||
|
|
||||||
///刷新状态
|
|
||||||
Future<void> _refresh() async {
|
|
||||||
await Future.delayed(Duration(seconds: 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final vm = context.read<SHomeVm>();
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
|
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
appBar: UserHeader(),
|
appBar: UserHeader(),
|
||||||
body: RefreshIndicator(
|
body: RefreshIndicator(
|
||||||
onRefresh: _refresh,
|
onRefresh: vm.loadData,
|
||||||
child: ListView(
|
child: ListView(
|
||||||
padding: EdgeInsets.all(context.pagePadding),
|
padding: EdgeInsets.all(context.pagePadding),
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import 'package:app/config/theme/base/app_theme_ext.dart';
|
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:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import '../viewmodel/s_home_vm.dart';
|
||||||
|
|
||||||
///banner
|
///banner
|
||||||
class BannerInfo extends StatelessWidget {
|
class BannerInfo extends StatelessWidget {
|
||||||
@@ -7,11 +11,12 @@ class BannerInfo extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final vm = context.read<SHomeVm>();
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: Image.network(
|
child: CachedNetworkImage(
|
||||||
"https://images.unsplash.com/photo-1505209487757-5114235191e5?w=800",
|
imageUrl: "https://www.gxgif.com/pic/fj/2025115155717.jpg",
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -35,34 +40,38 @@ class BannerInfo extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Visibility(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
visible: false,
|
||||||
margin: EdgeInsets.only(bottom: 30),
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||||
color: Colors.black26,
|
margin: EdgeInsets.only(bottom: 30),
|
||||||
borderRadius: BorderRadius.circular(30),
|
decoration: BoxDecoration(
|
||||||
),
|
color: Colors.black26,
|
||||||
child: Row(
|
borderRadius: BorderRadius.circular(30),
|
||||||
mainAxisSize: MainAxisSize.min,
|
),
|
||||||
spacing: 5,
|
child: Row(
|
||||||
children: [
|
mainAxisSize: MainAxisSize.min,
|
||||||
Container(
|
spacing: 5,
|
||||||
width: 15,
|
children: [
|
||||||
height: 15,
|
Container(
|
||||||
decoration: BoxDecoration(
|
width: 15,
|
||||||
color: context.success,
|
height: 15,
|
||||||
shape: BoxShape.circle,
|
decoration: BoxDecoration(
|
||||||
|
color: context.success,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
Text(
|
||||||
Text(
|
"进行中",
|
||||||
"进行中",
|
style: TextStyle(color: Colors.white, fontSize: 14),
|
||||||
style: TextStyle(color: Colors.white, fontSize: 14),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
SizedBox(height: 50),
|
||||||
Text(
|
Text(
|
||||||
"高中数学专场",
|
vm.roomInfo?.roomName ?? "",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 20,
|
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/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/router/route_paths.dart';
|
||||||
|
import 'package:app/utils/permission.dart';
|
||||||
import 'package:app/widgets/base/button/index.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/material.dart';
|
||||||
|
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||||
import 'package:go_router/go_router.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:remixicon/remixicon.dart';
|
||||||
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
|
|
||||||
import 'banner_info.dart';
|
import 'banner_info.dart';
|
||||||
import 'teacher_info.dart';
|
|
||||||
|
|
||||||
class STodayCard extends StatefulWidget {
|
class STodayCard extends StatefulWidget {
|
||||||
const STodayCard({super.key});
|
const STodayCard({super.key});
|
||||||
@@ -16,60 +23,124 @@ class STodayCard extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _STodayCardState extends State<STodayCard> {
|
class _STodayCardState extends State<STodayCard> {
|
||||||
///进入自习室
|
///前往会议室
|
||||||
void _handleEnterRoom() {
|
void _goToRoom() {
|
||||||
context.push(RoutePaths.sRoom);
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
final vm = context.watch<SHomeVm>();
|
||||||
clipBehavior: Clip.hardEdge,
|
if (!vm.loading && vm.roomInfo == null) {
|
||||||
decoration: BoxDecoration(
|
return Empty(text: "没有自习室");
|
||||||
borderRadius: BorderRadius.circular(10),
|
}
|
||||||
),
|
return Skeletonizer(
|
||||||
child: Column(
|
enabled: vm.loading,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
child: Container(
|
||||||
children: [
|
clipBehavior: Clip.hardEdge,
|
||||||
BannerInfo(),
|
decoration: BoxDecoration(
|
||||||
Container(
|
borderRadius: BorderRadius.circular(10),
|
||||||
width: double.infinity,
|
),
|
||||||
padding: EdgeInsets.symmetric(horizontal: context.pagePadding, vertical: 20),
|
child: Column(
|
||||||
decoration: BoxDecoration(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
color: Colors.white,
|
children: [
|
||||||
|
Skeleton.unite(
|
||||||
|
child: BannerInfo(),
|
||||||
),
|
),
|
||||||
child: Column(
|
Container(
|
||||||
spacing: 30,
|
width: double.infinity,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
padding: EdgeInsets.symmetric(horizontal: context.pagePadding, vertical: 20),
|
||||||
children: [
|
decoration: BoxDecoration(
|
||||||
TeacherInfo(),
|
color: Colors.white,
|
||||||
Row(
|
),
|
||||||
spacing: 20,
|
child: Column(
|
||||||
children: [
|
spacing: 30,
|
||||||
InfoItem(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
label: "自习时间",
|
children: [
|
||||||
value: "19:00-21:00",
|
Container(
|
||||||
icon: RemixIcons.time_line,
|
padding: EdgeInsets.all(context.pagePadding),
|
||||||
color: Theme.of(context).primaryColor,
|
decoration: BoxDecoration(
|
||||||
|
color: Color(0xffeef2ff),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
InfoItem(
|
child: Row(
|
||||||
label: "在线人数",
|
spacing: 15,
|
||||||
value: "8/12 人",
|
children: [
|
||||||
icon: RemixIcons.time_line,
|
Container(
|
||||||
color: context.success,
|
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/providers/user_store.dart';
|
||||||
import 'package:app/router/route_paths.dart';
|
import 'package:app/router/route_paths.dart';
|
||||||
|
import 'package:app/utils/time.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
@@ -10,6 +11,7 @@ class UserHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final userStore = context.read<UserStore>();
|
||||||
return AppBar(
|
return AppBar(
|
||||||
title: const Text('学光自习室'),
|
title: const Text('学光自习室'),
|
||||||
actions: [
|
actions: [
|
||||||
@@ -32,7 +34,7 @@ class UserHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
size: 18,
|
size: 18,
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
"会员至 2025-03-12",
|
"会员至 ${formatDate(userStore.userInfo?.extraInfo.vipEndTime,'YYYY-MM-DD')}",
|
||||||
style: TextStyle(color: Colors.white, fontSize: 14),
|
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:app/widgets/room/file_drawer.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
import 'package:remixicon/remixicon.dart';
|
import 'package:remixicon/remixicon.dart';
|
||||||
|
|
||||||
|
|
||||||
@@ -23,31 +25,41 @@ class _BottomBarState extends State<BottomBar> {
|
|||||||
color: Color(0xff232426),
|
color: Color(0xff232426),
|
||||||
),
|
),
|
||||||
height: 70,
|
height: 70,
|
||||||
child: Row(
|
child: Consumer<StuRoomVM>(
|
||||||
children: [
|
builder: (context,vm,_) {
|
||||||
BarItem(
|
//摄像头开关
|
||||||
title: "摄像头",
|
return Row(
|
||||||
icon: RemixIcons.video_on_fill,
|
children: [
|
||||||
),
|
BarItem(
|
||||||
BarItem(
|
title: "摄像头",
|
||||||
title: "麦克风",
|
icon: vm.cameraOpen ? RemixIcons.video_on_fill : RemixIcons.video_off_fill,
|
||||||
icon: RemixIcons.mic_off_fill,
|
isOff: !vm.cameraOpen,
|
||||||
),
|
onTap: vm.changeCameraSwitch,
|
||||||
BarItem(
|
),
|
||||||
title: "已静音",
|
BarItem(
|
||||||
icon: RemixIcons.volume_mute_fill,
|
title: "麦克风",
|
||||||
isOff: true,
|
icon: vm.micOpen ? RemixIcons.mic_fill : RemixIcons.mic_off_fill,
|
||||||
),
|
isOff: !vm.micOpen,
|
||||||
BarItem(
|
onTap: vm.changeMicSwitch,
|
||||||
title: "举手",
|
),
|
||||||
icon: RemixIcons.hand,
|
BarItem(
|
||||||
),
|
title: "声音",
|
||||||
BarItem(
|
icon: vm.speakerOpen ? RemixIcons.volume_up_fill : RemixIcons.volume_mute_fill,
|
||||||
title: "拍照",
|
isOff: !vm.speakerOpen,
|
||||||
icon: RemixIcons.upload_2_fill,
|
onTap: vm.changeSpeakerSwitch,
|
||||||
onTap: _handShowFile,
|
),
|
||||||
),
|
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:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
import 'package:remixicon/remixicon.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 bool showOther;
|
||||||
final void Function()? onOther;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final vm = context.read<StuRoomVM>();
|
||||||
|
|
||||||
return AppBar(
|
return AppBar(
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
titleTextStyle: TextStyle(color: Colors.white, fontSize: 18),
|
titleTextStyle: const TextStyle(color: Colors.white, fontSize: 18),
|
||||||
backgroundColor: Color(0xff232426),
|
backgroundColor: const Color(0xff232426),
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
title: Column(
|
title: Column(
|
||||||
children: [
|
children: [
|
||||||
Text("会议"),
|
Text(vm.roomInfo.roomName),
|
||||||
Text(
|
Text(
|
||||||
"01:12",
|
formatSeconds(seconds),
|
||||||
style: TextStyle(fontSize: 12, color: Colors.white24),
|
style: const TextStyle(fontSize: 12, color: Colors.white24),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: onOther,
|
onPressed: widget.onOther,
|
||||||
icon: Icon(showOther ? RemixIcons.team_fill : RemixIcons.team_line),
|
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:app/widgets/base/transition/slide_hide.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import 'controls/bottom_bar.dart';
|
import 'controls/bottom_bar.dart';
|
||||||
import 'controls/top_bar.dart';
|
import 'controls/top_bar.dart';
|
||||||
@@ -7,7 +11,9 @@ import 'video/student_video_list.dart';
|
|||||||
import 'video/teacher_video.dart';
|
import 'video/teacher_video.dart';
|
||||||
|
|
||||||
class SRoomPage extends StatefulWidget {
|
class SRoomPage extends StatefulWidget {
|
||||||
const SRoomPage({super.key});
|
final RoomInfoDto roomInfo;
|
||||||
|
|
||||||
|
const SRoomPage({super.key, required this.roomInfo});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<SRoomPage> createState() => _SRoomPageState();
|
State<SRoomPage> createState() => _SRoomPageState();
|
||||||
@@ -29,57 +35,63 @@ class _SRoomPageState extends State<SRoomPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
UserStore userStore = context.read<UserStore>();
|
||||||
body: Stack(
|
return ChangeNotifierProvider<StuRoomVM>(
|
||||||
children: [
|
create: (_) => StuRoomVM(
|
||||||
//底部控制显示
|
roomInfo: widget.roomInfo,
|
||||||
GestureDetector(
|
uid: userStore.userInfo!.id,
|
||||||
onTap: _toggleOverlay,
|
),
|
||||||
child: Container(color: Color(0xff2c3032)),
|
child: Scaffold(
|
||||||
),
|
body: Stack(
|
||||||
|
children: [
|
||||||
//老师视频画面
|
//底部控制显示
|
||||||
TeacherVideo(),
|
GestureDetector(
|
||||||
|
onTap: _toggleOverlay,
|
||||||
Positioned(
|
child: Container(color: Color(0xff2c3032)),
|
||||||
right: 0,
|
|
||||||
top: 0,
|
|
||||||
bottom: 0,
|
|
||||||
child: Visibility(
|
|
||||||
visible: _showOtherStudent,
|
|
||||||
child: StudentVideoList(),
|
|
||||||
),
|
),
|
||||||
),
|
//老师视频画面
|
||||||
|
TeacherVideo(),
|
||||||
|
|
||||||
///控制栏
|
Positioned(
|
||||||
Positioned(
|
right: 0,
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
bottom: 0,
|
||||||
right: 0,
|
child: Visibility(
|
||||||
child: SlideHide(
|
visible: _showOtherStudent,
|
||||||
direction: SlideDirection.up,
|
child: StudentVideoList(),
|
||||||
hide: !_controlsVisible,
|
|
||||||
child: TopBar(
|
|
||||||
showOther: _showOtherStudent,
|
|
||||||
onOther: () {
|
|
||||||
setState(() {
|
|
||||||
_showOtherStudent = !_showOtherStudent;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
Positioned(
|
///控制栏
|
||||||
bottom: 0,
|
Positioned(
|
||||||
left: 0,
|
top: 0,
|
||||||
right: 0,
|
left: 0,
|
||||||
child: SlideHide(
|
right: 0,
|
||||||
direction: SlideDirection.down,
|
child: SlideHide(
|
||||||
hide: !_controlsVisible,
|
direction: SlideDirection.up,
|
||||||
child: BottomBar(),
|
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:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class StudentVideoList extends StatefulWidget {
|
class StudentVideoList extends StatelessWidget {
|
||||||
const StudentVideoList({super.key});
|
const StudentVideoList({super.key});
|
||||||
|
|
||||||
@override
|
|
||||||
State<StudentVideoList> createState() => _StudentVideoListState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _StudentVideoListState extends State<StudentVideoList> {
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final vm = context.watch<StuRoomVM>();
|
||||||
return SafeArea(
|
return SafeArea(
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 250,
|
width: 250,
|
||||||
padding: EdgeInsets.only(bottom: 30),
|
padding: EdgeInsets.only(bottom: 30),
|
||||||
child: ListView.separated(
|
child: ListView.separated(
|
||||||
padding: EdgeInsets.all(10),
|
padding: EdgeInsets.all(10),
|
||||||
itemCount: 8,
|
itemCount: vm.otherStuList.length,
|
||||||
itemBuilder: (context, index) {
|
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),
|
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 {
|
int get roomMinutes {
|
||||||
if (roomInfo == null) return 0;
|
if (roomInfo == null) return 0;
|
||||||
|
final start = parseTime(roomInfo!.startTime);
|
||||||
|
final end = parseTime(roomInfo!.endTime);
|
||||||
|
|
||||||
final start = roomInfo!.startTime;
|
return end.difference(start).inMinutes;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
///能否进入房间
|
///能否进入房间
|
||||||
|
|||||||
@@ -28,13 +28,7 @@ class _TodayCardState extends State<TodayCard> {
|
|||||||
permissions: [Permission.microphone, Permission.camera],
|
permissions: [Permission.microphone, Permission.camera],
|
||||||
onGranted: () {
|
onGranted: () {
|
||||||
final vm = context.read<HomeViewModel>();
|
final vm = context.read<HomeViewModel>();
|
||||||
context.push(
|
context.push(RoutePaths.tRoom,extra: vm.roomInfo);
|
||||||
RoutePaths.tRoom,
|
|
||||||
extra: {
|
|
||||||
"roomId": vm.roomInfo!.id,
|
|
||||||
"startTime": vm.roomInfo!.startTime,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
onDenied: () {
|
onDenied: () {
|
||||||
EasyLoading.showError("请开启权限");
|
EasyLoading.showError("请开启权限");
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
|
import 'package:app/request/dto/room/room_info_dto.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'controls/top_bar.dart';
|
import 'controls/top_bar.dart';
|
||||||
import 'widgets/status_view.dart';
|
import 'widgets/status_view.dart';
|
||||||
import 'viewmodel/students_view_model.dart';
|
import 'viewmodel/tch_room_vm.dart';
|
||||||
|
|
||||||
class TRoomPage extends StatefulWidget {
|
class TRoomPage extends StatefulWidget {
|
||||||
final int roomId;
|
final RoomInfoDto roomInfo;
|
||||||
final String startTime;
|
|
||||||
|
|
||||||
const TRoomPage({
|
const TRoomPage({
|
||||||
super.key,
|
super.key,
|
||||||
required this.roomId,
|
required this.roomInfo,
|
||||||
required this.startTime,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -21,11 +20,10 @@ class TRoomPage extends StatefulWidget {
|
|||||||
class _TRoomPageState extends State<TRoomPage> {
|
class _TRoomPageState extends State<TRoomPage> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ChangeNotifierProvider<StudentsViewModel>(
|
return ChangeNotifierProvider<TchRoomVM>(
|
||||||
create: (BuildContext context) {
|
create: (BuildContext context) {
|
||||||
return StudentsViewModel(
|
return TchRoomVM(
|
||||||
roomId: widget.roomId,
|
roomInfo: widget.roomInfo,
|
||||||
start: widget.startTime,
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: Scaffold(
|
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/room_user_dto.dart';
|
||||||
import 'package:app/request/dto/room/rtc_token_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_protocol.dart';
|
||||||
@@ -7,18 +10,16 @@ import 'package:flutter/cupertino.dart';
|
|||||||
|
|
||||||
import 'type.dart';
|
import 'type.dart';
|
||||||
|
|
||||||
class StudentsViewModel extends ChangeNotifier {
|
class TchRoomVM extends ChangeNotifier {
|
||||||
StudentsViewModel({required this.roomId, String? start}) {
|
TchRoomVM({required this.roomInfo, String? start}) {
|
||||||
startTime = parseTime(start!);
|
|
||||||
_startRoom();
|
_startRoom();
|
||||||
}
|
}
|
||||||
|
|
||||||
///学生摄像头列表
|
///学生摄像头列表
|
||||||
List<RoomUserDto> _students = [];
|
List<RoomUserDto> _students = [];
|
||||||
|
|
||||||
///房间的基础信息,房间id、房间开始时间
|
///房间的基础信息
|
||||||
final int roomId;
|
final RoomInfoDto roomInfo;
|
||||||
late final DateTime startTime;
|
|
||||||
|
|
||||||
///老师选中的学生id
|
///老师选中的学生id
|
||||||
int activeSId = 0;
|
int activeSId = 0;
|
||||||
@@ -28,7 +29,7 @@ class StudentsViewModel extends ChangeNotifier {
|
|||||||
///是否能开始自习室
|
///是否能开始自习室
|
||||||
bool get canEnterRoom {
|
bool get canEnterRoom {
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
if (now.isAfter(startTime)) {
|
if (now.isAfter(parseTime(roomInfo.startTime))) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -36,6 +37,8 @@ class StudentsViewModel extends ChangeNotifier {
|
|||||||
|
|
||||||
///websocket管理
|
///websocket管理
|
||||||
final RoomWebSocket _ws = RoomWebSocket();
|
final RoomWebSocket _ws = RoomWebSocket();
|
||||||
|
bool wsConnected = false; // socket连接状态
|
||||||
|
StreamSubscription<RoomMessage>? _sub;
|
||||||
|
|
||||||
RtcTokenDto? get rtcToken => _ws.rtcToken;
|
RtcTokenDto? get rtcToken => _ws.rtcToken;
|
||||||
|
|
||||||
@@ -43,18 +46,16 @@ class StudentsViewModel extends ChangeNotifier {
|
|||||||
void _startRoom() async {
|
void _startRoom() async {
|
||||||
//如果socket的token没有,先初始化
|
//如果socket的token没有,先初始化
|
||||||
if (_ws.wsToken.isEmpty) {
|
if (_ws.wsToken.isEmpty) {
|
||||||
await _ws.initToken(roomId);
|
await _ws.initToken(roomInfo.id);
|
||||||
}
|
}
|
||||||
//启动连接
|
//启动连接
|
||||||
await _ws.connect();
|
await _ws.connect();
|
||||||
//进入房间命令
|
wsConnected = true;
|
||||||
_ws.send(RoomCommand.joinRoom);
|
|
||||||
|
|
||||||
//监听各种ws事件
|
//监听各种ws事件
|
||||||
_ws.stream.listen((msg) {
|
_sub = _ws.stream.listen((msg) {
|
||||||
// 自习室人员变化
|
// 自习室人员变化
|
||||||
if (msg.event == RoomEvent.changeUser) {
|
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);
|
onStudentChange(list);
|
||||||
} else if ([
|
} else if ([
|
||||||
RoomEvent.openSpeaker,
|
RoomEvent.openSpeaker,
|
||||||
@@ -65,8 +66,7 @@ class StudentsViewModel extends ChangeNotifier {
|
|||||||
RoomEvent.closeCamera,
|
RoomEvent.closeCamera,
|
||||||
RoomEvent.handUp,
|
RoomEvent.handUp,
|
||||||
].contains(msg.event)) {
|
].contains(msg.event)) {
|
||||||
onSyncStudentStatus();
|
onSyncStudentItem(RoomUserDto.fromJson(msg.data));
|
||||||
//TODO 直接同步服务器最新的数组覆盖,或者覆盖这一条学生的
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -91,29 +91,31 @@ class StudentsViewModel extends ChangeNotifier {
|
|||||||
///手动关闭学生扬声器、摄像头、麦克风等操作
|
///手动关闭学生扬声器、摄像头、麦克风等操作
|
||||||
/// - [uId]: 学生id
|
/// - [uId]: 学生id
|
||||||
/// - [action]: 操作类型
|
/// - [action]: 操作类型
|
||||||
void closeStudentSpeaker({
|
void closeStudentAction({
|
||||||
required int uId,
|
required int uId,
|
||||||
required StudentAction action,
|
required StudentAction action,
|
||||||
}) {
|
}) {
|
||||||
final student = _students.firstWhere((t) => t.userId == uId);
|
final student = _students.firstWhere((t) => t.userId == uId);
|
||||||
|
|
||||||
Map<String, int> data = {
|
Map<String, dynamic> data = {
|
||||||
'target_user_id': uId,
|
'target_user_id': uId,
|
||||||
|
"mute_type": action.value,
|
||||||
};
|
};
|
||||||
//如果是控制扬声器
|
//如果是控制扬声器
|
||||||
if (action == StudentAction.speaker) {
|
if (action == StudentAction.speaker) {
|
||||||
student.speekerStatus = student.speekerStatus == 0 ? 1 : 0;
|
bool isOpen = student.speekerStatus == 1;
|
||||||
data['speeker'] = student.speekerStatus;
|
student.speekerStatus = isOpen ? 0 : 1;
|
||||||
|
data['is_mute'] = isOpen ? 1 : 0;
|
||||||
} else if (action == StudentAction.camera) {
|
} else if (action == StudentAction.camera) {
|
||||||
//如果是摄像头,只能关
|
//如果是摄像头,只能关
|
||||||
if (student.cameraStatus == 0) return;
|
if (student.cameraStatus == 0) return;
|
||||||
student.cameraStatus = 0;
|
student.cameraStatus = 0;
|
||||||
data['camera'] = 0;
|
data['is_mute'] = 1;
|
||||||
} else if (action == StudentAction.microphone) {
|
} else if (action == StudentAction.microphone) {
|
||||||
//如果是麦克风,只能关
|
//如果是麦克风,只能关
|
||||||
if (student.microphoneStatus == 0) return;
|
if (student.microphoneStatus == 0) return;
|
||||||
student.microphoneStatus = 0;
|
student.microphoneStatus = 0;
|
||||||
data['microphone'] = 0;
|
data['is_mute'] = 1;
|
||||||
}
|
}
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
_ws.send(RoomCommand.switchStudentCamera, data);
|
_ws.send(RoomCommand.switchStudentCamera, data);
|
||||||
@@ -143,13 +145,21 @@ class StudentsViewModel extends ChangeNotifier {
|
|||||||
notifyListeners();
|
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
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
super.dispose();
|
super.dispose();
|
||||||
|
_sub?.cancel();
|
||||||
_ws.dispose();
|
_ws.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,15 @@
|
|||||||
///老师操作学生的状态、摄像头、扬声器、麦克风
|
///老师操作学生的状态、摄像头、扬声器、麦克风
|
||||||
enum StudentAction {
|
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:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import '../viewmodel/students_view_model.dart';
|
import '../viewmodel/tch_room_vm.dart';
|
||||||
import 'student_item.dart';
|
import 'student_item.dart';
|
||||||
|
|
||||||
class ContentView extends StatefulWidget {
|
class ContentView extends StatefulWidget {
|
||||||
@@ -14,19 +14,24 @@ class ContentView extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _ContentViewState extends State<ContentView> {
|
class _ContentViewState extends State<ContentView> {
|
||||||
// bool isLoading = true;
|
|
||||||
|
|
||||||
//声网数据
|
//声网数据
|
||||||
RtcEngine? _engine;
|
RtcEngine? _engine;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
// _initRtc();
|
||||||
|
}
|
||||||
|
|
||||||
void _initRtc() async {
|
void _initRtc() async {
|
||||||
final vm = context.read<StudentsViewModel>();
|
final vm = context.read<TchRoomVM>();
|
||||||
_engine = createAgoraRtcEngine();
|
_engine = createAgoraRtcEngine();
|
||||||
//初始化 RtcEngine,设置频道场景为 channelProfileLiveBroadcasting(直播场景)
|
//初始化 RtcEngine,设置频道场景为 channelProfileLiveBroadcasting(直播场景)
|
||||||
await _engine!.initialize(
|
await _engine!.initialize(
|
||||||
RtcEngineContext(
|
RtcEngineContext(
|
||||||
appId: Config.swAppId,
|
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) {},
|
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(
|
await _engine!.joinChannel(
|
||||||
token: vm.rtcToken!.token,
|
token: vm.rtcToken!.token,
|
||||||
channelId: vm.rtcToken!.channel,
|
channelId: vm.rtcToken!.channel,
|
||||||
uid: int.parse(vm.rtcToken!.uid),
|
uid: vm.rtcToken!.uid,
|
||||||
options: ChannelMediaOptions(
|
options: ChannelMediaOptions(
|
||||||
// 自动订阅所有视频流
|
// 自动订阅所有视频流
|
||||||
autoSubscribeVideo: true,
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Consumer<StudentsViewModel>(
|
return Consumer<TchRoomVM>(
|
||||||
builder: (context, vm, _) {
|
builder: (context, vm, _) {
|
||||||
if (vm.students.isEmpty) {
|
if (vm.students.isEmpty) {
|
||||||
return Center(
|
return Center(
|
||||||
child: Text('无学生在场,请通知学生入场'),
|
child: Text('准备中'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
//选中的学生
|
//选中的学生
|
||||||
@@ -96,6 +93,7 @@ class _ContentViewState extends State<ContentView> {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: StudentItem(
|
child: StudentItem(
|
||||||
user: activeStudent,
|
user: activeStudent,
|
||||||
|
engine: _engine,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
@@ -107,6 +105,7 @@ class _ContentViewState extends State<ContentView> {
|
|||||||
height: 250,
|
height: 250,
|
||||||
child: StudentItem(
|
child: StudentItem(
|
||||||
user: item,
|
user: item,
|
||||||
|
engine: _engine,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import 'content_view.dart';
|
import 'content_view.dart';
|
||||||
import '../viewmodel/students_view_model.dart';
|
import '../viewmodel/tch_room_vm.dart';
|
||||||
|
|
||||||
class StatusView extends StatefulWidget {
|
class StatusView extends StatefulWidget {
|
||||||
const StatusView({super.key});
|
const StatusView({super.key});
|
||||||
@@ -26,6 +26,8 @@ class _StatusViewState extends State<StatusView> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_init();
|
_init();
|
||||||
|
final vm = context.read<TchRoomVM>();
|
||||||
|
vm.addListener(openRoom);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -36,10 +38,11 @@ class _StatusViewState extends State<StatusView> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _init() {
|
void _init() {
|
||||||
final vm = context.read<StudentsViewModel>();
|
final vm = context.read<TchRoomVM>();
|
||||||
//如果房间可以开始
|
//如果房间到点可以开始
|
||||||
if (vm.canEnterRoom) {
|
if (vm.canEnterRoom) {
|
||||||
status = RoomStatus.start;
|
status = RoomStatus.start;
|
||||||
|
// openRoom();
|
||||||
} else {
|
} else {
|
||||||
status = RoomStatus.waiting;
|
status = RoomStatus.waiting;
|
||||||
startCountDown();
|
startCountDown();
|
||||||
@@ -48,12 +51,12 @@ class _StatusViewState extends State<StatusView> {
|
|||||||
|
|
||||||
///开始倒计时
|
///开始倒计时
|
||||||
void startCountDown() {
|
void startCountDown() {
|
||||||
final vm = context.read<StudentsViewModel>();
|
final vm = context.read<TchRoomVM>();
|
||||||
//当前时间
|
//当前时间
|
||||||
DateTime now = DateTime.now();
|
DateTime now = DateTime.now();
|
||||||
//远端时间
|
//远端时间
|
||||||
setState(() {
|
setState(() {
|
||||||
_seconds = vm.startTime.difference(now).inSeconds;
|
_seconds = parseTime(vm.roomInfo.startTime).difference(now).inSeconds;
|
||||||
});
|
});
|
||||||
_timer = Timer.periodic(Duration(seconds: 1), (timer) {
|
_timer = Timer.periodic(Duration(seconds: 1), (timer) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -71,8 +74,9 @@ class _StatusViewState extends State<StatusView> {
|
|||||||
|
|
||||||
///开启自习室
|
///开启自习室
|
||||||
void openRoom() {
|
void openRoom() {
|
||||||
final vm = context.read<StudentsViewModel>();
|
final vm = context.read<TchRoomVM>();
|
||||||
vm.toggleRoom(isOpen: true);
|
vm.toggleRoom(isOpen: true);
|
||||||
|
vm.removeListener(openRoom);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@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/request/dto/room/room_user_dto.dart';
|
||||||
import 'package:app/widgets/room/file_drawer.dart';
|
import 'package:app/widgets/room/file_drawer.dart';
|
||||||
import 'package:app/widgets/room/video_surface.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:provider/provider.dart';
|
||||||
import 'package:remixicon/remixicon.dart';
|
import 'package:remixicon/remixicon.dart';
|
||||||
|
|
||||||
import '../viewmodel/students_view_model.dart';
|
import '../viewmodel/tch_room_vm.dart';
|
||||||
|
|
||||||
class StudentItem extends StatefulWidget {
|
class StudentItem extends StatefulWidget {
|
||||||
final RoomUserDto user;
|
final RoomUserDto user;
|
||||||
|
final RtcEngine? engine;
|
||||||
|
|
||||||
const StudentItem({
|
const StudentItem({
|
||||||
super.key,
|
super.key,
|
||||||
required this.user,
|
required this.user,
|
||||||
|
this.engine,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -27,12 +31,16 @@ class _StudentItemState extends State<StudentItem> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final vm = context.read<StudentsViewModel>();
|
final vm = context.read<TchRoomVM>();
|
||||||
//摄像头是否开启
|
//摄像头是否开启
|
||||||
bool isCameraOpen = widget.user.cameraStatus == 1;
|
bool isCameraOpen = widget.user.cameraStatus == 1;
|
||||||
|
|
||||||
///麦克风是否开启
|
///麦克风是否开启
|
||||||
bool isMicOpen = widget.user.microphoneStatus == 1;
|
bool isMicOpen = widget.user.microphoneStatus == 1;
|
||||||
|
|
||||||
|
///声音是否开启
|
||||||
|
bool isSpeakerOpen = widget.user.speekerStatus == 1;
|
||||||
|
|
||||||
return ClipRRect(
|
return ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
child: Container(
|
child: Container(
|
||||||
@@ -44,6 +52,13 @@ class _StudentItemState extends State<StudentItem> {
|
|||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
|
if (widget.engine != null)
|
||||||
|
AgoraVideoView(
|
||||||
|
controller: VideoViewController(
|
||||||
|
rtcEngine: widget.engine!,
|
||||||
|
canvas: VideoCanvas(uid: widget.user.rtcUid),
|
||||||
|
),
|
||||||
|
),
|
||||||
// VideoSurface(),
|
// VideoSurface(),
|
||||||
Positioned(
|
Positioned(
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
@@ -95,14 +110,35 @@ class _StudentItemState extends State<StudentItem> {
|
|||||||
_actionItem(
|
_actionItem(
|
||||||
icon: isCameraOpen ? RemixIcons.video_on_fill : RemixIcons.video_off_fill,
|
icon: isCameraOpen ? RemixIcons.video_on_fill : RemixIcons.video_off_fill,
|
||||||
isActive: isCameraOpen,
|
isActive: isCameraOpen,
|
||||||
|
onTap: () {
|
||||||
|
vm.closeStudentAction(
|
||||||
|
uId: widget.user.userId,
|
||||||
|
action: StudentAction.camera,
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
_actionItem(
|
_actionItem(
|
||||||
icon: isMicOpen ? RemixIcons.mic_fill : RemixIcons.mic_off_fill,
|
icon: isMicOpen ? RemixIcons.mic_fill : RemixIcons.mic_off_fill,
|
||||||
isActive: isMicOpen,
|
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),
|
_actionItem(icon: RemixIcons.file_list_3_fill, onTap: _openFileList),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -7,21 +7,17 @@ class UserStore extends ChangeNotifier {
|
|||||||
UserInfoDto? userInfo;
|
UserInfoDto? userInfo;
|
||||||
String token = "";
|
String token = "";
|
||||||
|
|
||||||
///设置用户数据
|
Future<void> init() async{
|
||||||
Future<void> asyncUserInfo() async {
|
token = await getToken();
|
||||||
if (token.isNotEmpty) {
|
await setUserInfo();
|
||||||
var res = await getUserInfoApi();
|
notifyListeners();
|
||||||
await Storage.set("user_info", res.toJson());
|
|
||||||
setUserInfo();
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
///获取用户数据
|
///获取用户数据
|
||||||
Future<void> setUserInfo() async {
|
Future<void> setUserInfo() async {
|
||||||
var info = await Storage.get("user_info");
|
if (token.isNotEmpty) {
|
||||||
if (info != null) {
|
userInfo = await getUserInfoApi();
|
||||||
userInfo = UserInfoDto.fromJson(info);
|
await Storage.set("user_info", userInfo!.toJson());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,15 +28,16 @@ class UserStore extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
///获取token
|
///获取token
|
||||||
static Future<String> getToken() async {
|
static Future<String> getToken() async {
|
||||||
return await Storage.get("token") ?? '';
|
return await Storage.get("token") ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
///退出登录
|
///退出登录
|
||||||
Future<void> logout() async {
|
Future<void> logout() async {
|
||||||
logoutApi();
|
await logoutApi();
|
||||||
await Storage.remove('token');
|
await Storage.remove('token');
|
||||||
await Storage.remove('user_info');
|
await Storage.remove('user_info');
|
||||||
|
userInfo = null;
|
||||||
token = '';
|
token = '';
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
class RoomInfoDto {
|
class RoomInfoDto {
|
||||||
|
|
||||||
|
|
||||||
RoomInfoDto({
|
RoomInfoDto({
|
||||||
required this.teacherBackground,
|
required this.teacherBackground,
|
||||||
|
required this.teacherAvatar,
|
||||||
required this.roomName,
|
required this.roomName,
|
||||||
required this.startTime,
|
required this.startTime,
|
||||||
required this.teacherName,
|
required this.teacherName,
|
||||||
@@ -11,6 +10,7 @@ class RoomInfoDto {
|
|||||||
});
|
});
|
||||||
|
|
||||||
String teacherBackground;
|
String teacherBackground;
|
||||||
|
String teacherAvatar;
|
||||||
String roomName;
|
String roomName;
|
||||||
String startTime;
|
String startTime;
|
||||||
String teacherName;
|
String teacherName;
|
||||||
@@ -20,6 +20,7 @@ class RoomInfoDto {
|
|||||||
factory RoomInfoDto.fromJson(Map<dynamic, dynamic> json) =>
|
factory RoomInfoDto.fromJson(Map<dynamic, dynamic> json) =>
|
||||||
RoomInfoDto(
|
RoomInfoDto(
|
||||||
teacherBackground: json["teacher_background"],
|
teacherBackground: json["teacher_background"],
|
||||||
|
teacherAvatar: json["teacher_avatar"],
|
||||||
roomName: json["room_name"],
|
roomName: json["room_name"],
|
||||||
startTime: json["start_time"],
|
startTime: json["start_time"],
|
||||||
teacherName: json["teacher_name"],
|
teacherName: json["teacher_name"],
|
||||||
@@ -30,6 +31,7 @@ class RoomInfoDto {
|
|||||||
Map<dynamic, dynamic> toJson() =>
|
Map<dynamic, dynamic> toJson() =>
|
||||||
{
|
{
|
||||||
"teacher_background": teacherBackground,
|
"teacher_background": teacherBackground,
|
||||||
|
"teacher_avatar": teacherAvatar,
|
||||||
"room_name": roomName,
|
"room_name": roomName,
|
||||||
"start_time": startTime,
|
"start_time": startTime,
|
||||||
"teacher_name": teacherName,
|
"teacher_name": teacherName,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
class RoomTypeDto {
|
class RoomTypeDto {
|
||||||
final int studyRoomId;
|
final int studyRoomId;
|
||||||
final int teacherId;
|
final int teacherId;
|
||||||
final String teacherRtcUid;
|
final int teacherRtcUid;
|
||||||
final String teacherWsClientId;
|
final String teacherWsClientId;
|
||||||
final int roomStatus;
|
final int roomStatus;
|
||||||
final String dataType;
|
final String dataType;
|
||||||
@@ -30,7 +30,7 @@ class RoomTypeDto {
|
|||||||
return RoomTypeDto(
|
return RoomTypeDto(
|
||||||
studyRoomId: json["study_room_id"] ?? 0,
|
studyRoomId: json["study_room_id"] ?? 0,
|
||||||
teacherId: json["teacher_id"] ?? 0,
|
teacherId: json["teacher_id"] ?? 0,
|
||||||
teacherRtcUid: json["teacher_rtc_uid"] ?? "",
|
teacherRtcUid: json["teacher_rtc_uid"] ?? 0,
|
||||||
teacherWsClientId: json["teacher_ws_client_id"] ?? "",
|
teacherWsClientId: json["teacher_ws_client_id"] ?? "",
|
||||||
roomStatus: json["room_status"] ?? 0,
|
roomStatus: json["room_status"] ?? 0,
|
||||||
dataType: json["data_type"] ?? "",
|
dataType: json["data_type"] ?? "",
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
class RoomUserDto {
|
class RoomUserDto {
|
||||||
final int userId;
|
final int userId;
|
||||||
final String rtcUid;
|
final int rtcUid;
|
||||||
int microphoneStatus;
|
int microphoneStatus;
|
||||||
int cameraStatus;
|
int cameraStatus;
|
||||||
int speekerStatus;
|
int speekerStatus;
|
||||||
final String wsClientId;
|
final String wsClientId;
|
||||||
final String userName;
|
final String userName;
|
||||||
final String avatar;
|
final String avatar;
|
||||||
|
|
||||||
/// 1是学生,2是老师
|
/// 1是学生,2是老师
|
||||||
final int userType;
|
final int userType;
|
||||||
final List<String> filesList;
|
final List<String> filesList;
|
||||||
final String dataType;
|
final String dataType;
|
||||||
int handup;
|
int handup;
|
||||||
int online; //0离线,1在线
|
int online; //0离线,1在线
|
||||||
|
|
||||||
RoomUserDto({
|
RoomUserDto({
|
||||||
required this.userId,
|
required this.userId,
|
||||||
required this.rtcUid,
|
required this.rtcUid,
|
||||||
required this.microphoneStatus,
|
required this.microphoneStatus,
|
||||||
@@ -65,4 +66,12 @@ class RoomUserDto {
|
|||||||
"online": online,
|
"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,
|
required this.token,
|
||||||
});
|
});
|
||||||
|
|
||||||
String uid;
|
int uid;
|
||||||
DateTime expiresAt;
|
DateTime expiresAt;
|
||||||
String channel;
|
String channel;
|
||||||
String token;
|
String token;
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ void onResponse(
|
|||||||
error: {'code': 0, 'message': apiResponse.message},
|
error: {'code': 0, 'message': apiResponse.message},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
showError(apiResponse.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ enum RoomCommand {
|
|||||||
getRoomInfo("room_data"),
|
getRoomInfo("room_data"),
|
||||||
|
|
||||||
///学生开关扬声器、摄像头、麦克风
|
///学生开关扬声器、摄像头、麦克风
|
||||||
switchCamera("mute_self"),
|
studentActon("mute_self"),
|
||||||
|
|
||||||
///学生上传文件
|
///学生上传文件
|
||||||
uploadFile("upload_file"),
|
uploadFile("upload_file"),
|
||||||
@@ -66,7 +66,7 @@ enum RoomEvent {
|
|||||||
handUp("sys_user_handup"),
|
handUp("sys_user_handup"),
|
||||||
|
|
||||||
///自习室以开启,进入自习室(学生用)
|
///自习室以开启,进入自习室(学生用)
|
||||||
openRoom("sys_start_study_room"),
|
// openRoom("sys_start_study_room"),
|
||||||
|
|
||||||
///自习室以关闭,退出自习室(学生用)
|
///自习室以关闭,退出自习室(学生用)
|
||||||
closeRoom("sys_close_study_room"),
|
closeRoom("sys_close_study_room"),
|
||||||
@@ -94,10 +94,10 @@ enum RoomEvent {
|
|||||||
const RoomEvent(this.value);
|
const RoomEvent(this.value);
|
||||||
|
|
||||||
/// 根据 值获取枚举
|
/// 根据 值获取枚举
|
||||||
static RoomEvent fromStr(String value) {
|
static RoomEvent? fromStr(String value) {
|
||||||
return RoomEvent.values.firstWhere(
|
for (final e in RoomEvent.values) {
|
||||||
(e) => e.value == value,
|
if (e.value == value) return e;
|
||||||
orElse: () => throw ArgumentError('Invalid weather type value: $value'),
|
}
|
||||||
);
|
return null; // 找不到就返回 null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,7 +60,15 @@ class RoomWebSocket {
|
|||||||
(data) {
|
(data) {
|
||||||
//监听事件
|
//监听事件
|
||||||
final jsonMap = jsonDecode(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);
|
_msgController.add(msg);
|
||||||
},
|
},
|
||||||
onDone: () {},
|
onDone: () {},
|
||||||
@@ -68,10 +76,12 @@ class RoomWebSocket {
|
|||||||
logger.e("连接异常断开");
|
logger.e("连接异常断开");
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
//自动加入房间
|
||||||
|
send(RoomCommand.joinRoom);
|
||||||
|
|
||||||
//心跳
|
//心跳
|
||||||
_heartbeatTimer?.cancel();
|
_heartbeatTimer?.cancel();
|
||||||
_heartbeatTimer = Timer.periodic(Duration(seconds: 15), (_) {
|
_heartbeatTimer = Timer.periodic(Duration(seconds: 15), (_) {
|
||||||
logger.i("发送心跳");
|
|
||||||
send(RoomCommand.ping);
|
send(RoomCommand.ping);
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -84,8 +94,12 @@ class RoomWebSocket {
|
|||||||
void send(RoomCommand action, [Map<String, dynamic>? params]) {
|
void send(RoomCommand action, [Map<String, dynamic>? params]) {
|
||||||
final msg = {
|
final msg = {
|
||||||
"action": action.value,
|
"action": action.value,
|
||||||
"data": params,
|
if (params != null) ...params,
|
||||||
};
|
};
|
||||||
|
if(action != RoomCommand.ping){
|
||||||
|
logger.i("发送指令:$msg");
|
||||||
|
}
|
||||||
|
|
||||||
_socket!.add(jsonEncode(msg));
|
_socket!.add(jsonEncode(msg));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,7 +119,7 @@ class RoomWebSocket {
|
|||||||
//socket取消
|
//socket取消
|
||||||
_socket?.close();
|
_socket?.close();
|
||||||
// 销毁事件流
|
// 销毁事件流
|
||||||
_msgController.close();
|
// _msgController.close();
|
||||||
// 错误重连取消
|
// 错误重连取消
|
||||||
_reconnectTimer?.cancel();
|
_reconnectTimer?.cancel();
|
||||||
_reconnectTimer = null;
|
_reconnectTimer = null;
|
||||||
|
|||||||
@@ -14,7 +14,10 @@ List<RouterConfig> studentRoutes = [
|
|||||||
RouterConfig(
|
RouterConfig(
|
||||||
path: RoutePaths.sRoom,
|
path: RoutePaths.sRoom,
|
||||||
child: (state) {
|
child: (state) {
|
||||||
return SRoomPage();
|
final extra = state.extra as dynamic;
|
||||||
|
return SRoomPage(
|
||||||
|
roomInfo: extra,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -14,12 +14,9 @@ List<RouterConfig> teacherRoutes = [
|
|||||||
RouterConfig(
|
RouterConfig(
|
||||||
path: RoutePaths.tRoom,
|
path: RoutePaths.tRoom,
|
||||||
child: (state) {
|
child: (state) {
|
||||||
final extra = state.extra as Map<String, dynamic>?;
|
final extra = state.extra as dynamic;
|
||||||
final roomId = extra?['roomId'] as int?;
|
|
||||||
final startTime = extra?['startTime'] as String?;
|
|
||||||
return TRoomPage(
|
return TRoomPage(
|
||||||
roomId: roomId!,
|
roomInfo: extra,
|
||||||
startTime: startTime!,
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -21,60 +21,26 @@ enum VideoState {
|
|||||||
error,
|
error,
|
||||||
}
|
}
|
||||||
|
|
||||||
// class VideoSurface extends StatelessWidget {
|
class VideoSurface extends StatelessWidget {
|
||||||
// final VideoState state;
|
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 StatefulWidget {
|
const VideoSurface({super.key, this.state = VideoState.normal});
|
||||||
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,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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