diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts
index 932246b..31b7d05 100644
--- a/android/app/build.gradle.kts
+++ b/android/app/build.gradle.kts
@@ -6,7 +6,7 @@ plugins {
}
android {
- namespace = "com.zkwl.xueguang.xueguang_flutter_app"
+ namespace = "com.zkwl.xueguang"
compileSdk = flutter.compileSdkVersion
ndkVersion = "27.0.12077973"
@@ -21,7 +21,7 @@ android {
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
- applicationId = "com.zkwl.xueguang.xueguang_flutter_app"
+ applicationId = "com.zkwl.xueguang"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index d43376a..fa2d3e6 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -1,28 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ android:icon="@mipmap/ic_launcher"
+ android:label="学光自习室">
+ android:name="io.flutter.embedding.android.NormalTheme"
+ android:resource="@style/NormalTheme" />
-
-
+
+
-
-
+
+
diff --git a/android/app/src/main/kotlin/com/zkwl/xueguang/xueguang_flutter_app/MainActivity.kt b/android/app/src/main/kotlin/com/zkwl/xueguang/xueguang_flutter_app/MainActivity.kt
index 0a78501..b4082f3 100644
--- a/android/app/src/main/kotlin/com/zkwl/xueguang/xueguang_flutter_app/MainActivity.kt
+++ b/android/app/src/main/kotlin/com/zkwl/xueguang/xueguang_flutter_app/MainActivity.kt
@@ -1,4 +1,4 @@
-package com.zkwl.xueguang.xueguang_flutter_app
+package com.zkwl.xueguang
import io.flutter.embedding.android.FlutterActivity
diff --git a/assets/image/empty_data.png b/assets/image/empty_data.png
new file mode 100644
index 0000000..d328a99
Binary files /dev/null and b/assets/image/empty_data.png differ
diff --git a/devtools_options.yaml b/devtools_options.yaml
new file mode 100644
index 0000000..fa0b357
--- /dev/null
+++ b/devtools_options.yaml
@@ -0,0 +1,3 @@
+description: This file stores settings for Dart & Flutter DevTools.
+documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
+extensions:
diff --git a/lib/config/config.dart b/lib/config/config.dart
index 9b5a7f5..60ea124 100644
--- a/lib/config/config.dart
+++ b/lib/config/config.dart
@@ -8,9 +8,17 @@ class Config {
///获取接口地址
static String baseUrl() {
if (getEnv() == 'dev') {
- return 'https://mindapp.test.tuzuu.com/api';
+ return 'https://xueguang.test.tuzuu.com/api';
} else {
- return 'https://mindapp.cells.org.cn/api';
+ return 'https://xueguang.test.tuzuu.com/api';
}
}
+
+ /// 获取websocket地址
+ static String wsUrl() {
+ return "wss://xueguang.test.tuzuu.com/ws";
+ }
+
+ ///声网APPid
+ static String get swAppId => "011c2fd2e1854511a80c1aebded4eee7";
}
diff --git a/lib/data/local/storage.dart b/lib/data/local/storage.dart
new file mode 100644
index 0000000..f7740fb
--- /dev/null
+++ b/lib/data/local/storage.dart
@@ -0,0 +1,50 @@
+import 'dart:convert';
+
+import 'package:shared_preferences/shared_preferences.dart';
+
+
+class Storage {
+
+ //存储数据
+ static Future set(String key, dynamic value) async {
+ SharedPreferences sp = await SharedPreferences.getInstance();
+ if (value is String) {
+ sp.setString(key, value);
+ } else if (value is int) {
+ sp.setInt(key, value);
+ } else if (value is bool) {
+ sp.setBool(key, value);
+ } else if (value is double) {
+ sp.setDouble(key, value);
+ } else if (value is Map) {
+ String jsonStr = jsonEncode(value);
+ sp.setString(key, jsonStr);
+ }
+ }
+
+ //获取数据
+ static Future get(String key) async {
+ SharedPreferences sp = await SharedPreferences.getInstance();
+ var value = sp.get(key);
+ if (value is String) {
+ try {
+ return jsonDecode(value);
+ } catch (e) {
+ return value;
+ }
+ }
+ return value;
+ }
+
+ //删除数据
+ static Future remove(key) async {
+ SharedPreferences sp = await SharedPreferences.getInstance();
+ sp.remove(key);
+ }
+
+//判断键是否存在
+ static Future hasKey(String key) async {
+ SharedPreferences sp = await SharedPreferences.getInstance();
+ return sp.containsKey(key);
+ }
+}
diff --git a/lib/main.dart b/lib/main.dart
index df250e7..63a01e7 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -1,13 +1,22 @@
+import 'package:app/providers/user_store.dart';
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:app/router/routes.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:provider/provider.dart';
import 'config/theme/theme.dart';
void main() {
- runApp(const MyApp());
+ runApp(
+ MultiProvider(
+ providers: [
+ ChangeNotifierProvider(create: (_) => UserStore()),
+ ],
+ child: const MyApp(),
+ ),
+ );
}
class MyApp extends StatelessWidget {
diff --git a/lib/pages/common/auth/login_page.dart b/lib/pages/common/auth/login_page.dart
index a0c1f23..e2e9702 100644
--- a/lib/pages/common/auth/login_page.dart
+++ b/lib/pages/common/auth/login_page.dart
@@ -1,14 +1,16 @@
import 'dart:async';
+import 'package:app/providers/user_store.dart';
+import 'package:app/request/api/user_api.dart';
import 'package:app/router/route_paths.dart';
import 'package:app/widgets/base/button/index.dart';
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:go_router/go_router.dart';
+import 'package:provider/provider.dart';
import 'package:remixicon/remixicon.dart';
-import 'widgets/login_agree.dart';
import 'widgets/login_input.dart';
class LoginPage extends StatefulWidget {
@@ -23,8 +25,11 @@ class _LoginPageState extends State {
bool _agree = false;
///输入框
- final TextEditingController _telController = TextEditingController();
- final TextEditingController _codeController = TextEditingController();
+ final TextEditingController _telController = TextEditingController(text: "13343214321");
+ final TextEditingController _codeController = TextEditingController(text: "1111");
+
+ ///登录中
+ bool _loading = false;
///验证码倒计时
var _countDown = 0;
@@ -42,6 +47,7 @@ class _LoginPageState extends State {
EasyLoading.showToast("请填写正确的手机号");
return;
}
+ sendCodeApi(_telController.text);
setState(() {
_countDown = 60;
});
@@ -63,7 +69,29 @@ class _LoginPageState extends State {
EasyLoading.showToast("请填写完整手机号或验证码");
return;
}
- context.go(RoutePaths.sHome);
+ try {
+ setState(() {
+ _loading = true;
+ });
+ var loginRes = await loginApi(_telController.text, _codeController.text);
+ if (mounted) {
+ UserStore userStore = context.read();
+
+ //设置登录信息l
+ await userStore.setToken(loginRes.accessToken);
+ await userStore.asyncUserInfo();
+ if (!mounted) return;
+ if (userStore.userInfo?.accountType == 1) {
+ context.go(RoutePaths.sHome);
+ } else {
+ context.go(RoutePaths.sHome);
+ }
+ }
+ } finally {
+ setState(() {
+ _loading = false;
+ });
+ }
}
@override
@@ -130,21 +158,25 @@ class _LoginPageState extends State {
Container(
margin: EdgeInsets.only(top: 40),
height: 50,
- child: Button(text: "登 录", onPressed: _handSubmit),
- ),
- Container(
- width: double.infinity,
- margin: EdgeInsets.only(top: 20),
- alignment: Alignment.center,
- child: LoginAgree(
- value: _agree,
- onChanged: (value) {
- setState(() {
- _agree = value!;
- });
- },
+ child: Button(
+ text: "登 录",
+ loading: _loading,
+ onPressed: _handSubmit,
),
),
+ // Container(
+ // width: double.infinity,
+ // margin: EdgeInsets.only(top: 20),
+ // alignment: Alignment.center,
+ // child: LoginAgree(
+ // value: _agree,
+ // onChanged: (value) {
+ // setState(() {
+ // _agree = value!;
+ // });
+ // },
+ // ),
+ // ),
],
),
),
diff --git a/lib/pages/common/splash/splash_page.dart b/lib/pages/common/splash/splash_page.dart
index 10fc100..fc7c125 100644
--- a/lib/pages/common/splash/splash_page.dart
+++ b/lib/pages/common/splash/splash_page.dart
@@ -1,6 +1,8 @@
+import 'package:app/providers/user_store.dart';
import 'package:app/router/route_paths.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
+import 'package:provider/provider.dart';
class SplashPage extends StatefulWidget {
const SplashPage({super.key});
@@ -19,7 +21,24 @@ class _SplashPageState extends State {
///权限效验初始化
void initPermission() async {
WidgetsBinding.instance.addPostFrameCallback((_) async {
- context.go(RoutePaths.login);
+ String token = await UserStore.getToken();
+ if (mounted) {
+ // 未登录
+ if (token.isEmpty) {
+ context.go(RoutePaths.login);
+ } else {
+ UserStore userStore = context.read();
+ userStore.setUserInfo();
+ //去学生主页
+ if (userStore.userInfo?.accountType == 1) {
+ context.go(RoutePaths.sHome);
+ } else {
+ context.go(RoutePaths.tHome);
+ }
+ print("执行用户数据同步了");
+ userStore.asyncUserInfo();
+ }
+ }
});
}
diff --git a/lib/pages/student/home/s_home_page.dart b/lib/pages/student/home/s_home_page.dart
index 822bf4b..44002e9 100644
--- a/lib/pages/student/home/s_home_page.dart
+++ b/lib/pages/student/home/s_home_page.dart
@@ -1,4 +1,6 @@
import 'package:app/config/theme/base/app_theme_ext.dart';
+import 'package:app/request/api/room_api.dart';
+import 'package:app/request/dto/room/room_type_dto.dart';
import 'package:flutter/material.dart';
import 'today/s_today_card.dart';
@@ -12,6 +14,8 @@ class SHomePage extends StatefulWidget {
}
class _SHomePageState extends State {
+
+
///刷新状态
Future _refresh() async {
await Future.delayed(Duration(seconds: 1));
diff --git a/lib/pages/student/home/today/s_today_card.dart b/lib/pages/student/home/today/s_today_card.dart
index c8f8b94..86613bf 100644
--- a/lib/pages/student/home/today/s_today_card.dart
+++ b/lib/pages/student/home/today/s_today_card.dart
@@ -16,7 +16,6 @@ class STodayCard extends StatefulWidget {
}
class _STodayCardState extends State {
-
///进入自习室
void _handleEnterRoom() {
context.push(RoutePaths.sRoom);
diff --git a/lib/pages/student/home/widgets/user_header.dart b/lib/pages/student/home/widgets/user_header.dart
index 9ac7159..6543b4f 100644
--- a/lib/pages/student/home/widgets/user_header.dart
+++ b/lib/pages/student/home/widgets/user_header.dart
@@ -1,4 +1,8 @@
+import 'package:app/providers/user_store.dart';
+import 'package:app/router/route_paths.dart';
import 'package:flutter/material.dart';
+import 'package:go_router/go_router.dart';
+import 'package:provider/provider.dart';
import 'package:remixicon/remixicon.dart';
class UserHeader extends StatelessWidget implements PreferredSizeWidget {
@@ -34,7 +38,28 @@ class UserHeader extends StatelessWidget implements PreferredSizeWidget {
],
),
),
- const SizedBox(width: 15),
+ PopupMenuButton(
+ color: Colors.white,
+ padding: EdgeInsets.zero,
+ position: PopupMenuPosition.under,
+ onSelected: (value) {
+ if (value == 1) {
+ UserStore userStore = context.read();
+ userStore.logout();
+ context.go(RoutePaths.login);
+ }
+ },
+ itemBuilder: (context) => [
+ PopupMenuItem(
+ value: 1,
+ child: Text("退出登录", textAlign: TextAlign.center),
+ ),
+ ],
+ child: IconButton(
+ onPressed: null,
+ icon: Icon(RemixIcons.user_line),
+ ),
+ ),
],
);
}
diff --git a/lib/pages/teacher/home/t_home_page.dart b/lib/pages/teacher/home/t_home_page.dart
index 0fef0cc..8fbeb49 100644
--- a/lib/pages/teacher/home/t_home_page.dart
+++ b/lib/pages/teacher/home/t_home_page.dart
@@ -1,27 +1,43 @@
import 'package:app/config/theme/base/app_theme_ext.dart';
import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+import 'viewmodel/home_view_model.dart';
import 'widgets/header.dart';
import 'widgets/today_card.dart';
-class THomePage extends StatefulWidget {
+class THomePage extends StatelessWidget {
const THomePage({super.key});
@override
- State createState() => _THomePageState();
+ Widget build(BuildContext context) {
+ return ChangeNotifierProvider(
+ create: (_) => HomeViewModel(),
+ child: const _HomeView(),
+ );
+ }
}
-class _THomePageState extends State {
+class _HomeView extends StatelessWidget {
+ const _HomeView();
+
@override
Widget build(BuildContext context) {
+ final vm = context.read();
return Scaffold(
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
appBar: Header(),
- body: ListView(
- padding: EdgeInsets.symmetric(vertical: 20, horizontal: context.pagePadding),
- children: [
- TodayCard(),
- ],
+ body: RefreshIndicator(
+ onRefresh: vm.loadData,
+ child: ListView(
+ padding: EdgeInsets.symmetric(
+ vertical: 20,
+ horizontal: context.pagePadding,
+ ),
+ children: [
+ TodayCard(),
+ ],
+ ),
),
);
}
diff --git a/lib/pages/teacher/home/viewmodel/home_view_model.dart b/lib/pages/teacher/home/viewmodel/home_view_model.dart
new file mode 100644
index 0000000..1f06b5d
--- /dev/null
+++ b/lib/pages/teacher/home/viewmodel/home_view_model.dart
@@ -0,0 +1,56 @@
+import 'package:app/request/api/room_api.dart';
+import 'package:app/request/dto/room/room_info_dto.dart';
+import 'package:app/utils/time.dart';
+import 'package:flutter/material.dart';
+
+class HomeViewModel extends ChangeNotifier {
+ RoomInfoDto? roomInfo;
+ bool loading = true;
+
+ HomeViewModel() {
+ loadData();
+ }
+
+ //加载数据
+ Future loadData() async {
+ final list = await getRoomListApi();
+ loading = false;
+
+ if (list.isNotEmpty) {
+ roomInfo = list.first;
+ }
+
+ notifyListeners();
+ }
+
+ ///计算会议时间
+ int get roomMinutes {
+ if (roomInfo == null) return 0;
+
+ final start = roomInfo!.startTime;
+ final end = roomInfo!.endTime;
+
+ final s = DateTime.parse('2000-01-01 $start:00');
+ final e = DateTime.parse('2000-01-01 $end:00');
+
+ return e.difference(s).inMinutes;
+ }
+
+ ///能否进入房间
+ bool get canEnterRoom {
+ final info = roomInfo;
+ if (info == null) return false;
+
+ final now = DateTime.now();
+
+ //开始时间
+ final startTime = parseTime(info.startTime);
+
+ // 当前时间距离开始时间是否超过 5 分钟
+ if (now.isBefore(startTime) && startTime.difference(now).inMinutes > 5) {
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/lib/pages/teacher/home/widgets/header.dart b/lib/pages/teacher/home/widgets/header.dart
index 1013b77..1d19764 100644
--- a/lib/pages/teacher/home/widgets/header.dart
+++ b/lib/pages/teacher/home/widgets/header.dart
@@ -1,5 +1,9 @@
import 'package:app/config/theme/base/app_theme_ext.dart';
+import 'package:app/providers/user_store.dart';
+import 'package:app/router/route_paths.dart';
import 'package:flutter/material.dart';
+import 'package:go_router/go_router.dart';
+import 'package:provider/provider.dart';
import 'package:remixicon/remixicon.dart';
class Header extends StatelessWidget implements PreferredSizeWidget {
@@ -18,6 +22,7 @@ class Header extends StatelessWidget implements PreferredSizeWidget {
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"学光自习室",
@@ -31,9 +36,27 @@ class Header extends StatelessWidget implements PreferredSizeWidget {
),
Row(
children: [
- IconButton(
- onPressed: () {},
- icon: Icon(RemixIcons.user_line),
+ PopupMenuButton(
+ color: Colors.white,
+ padding: EdgeInsets.zero,
+ position: PopupMenuPosition.under,
+ onSelected: (value) {
+ if (value == 1) {
+ UserStore userStore = context.read();
+ userStore.logout();
+ context.go(RoutePaths.login);
+ }
+ },
+ itemBuilder: (context) => [
+ PopupMenuItem(
+ value: 1,
+ child: Text("退出登录", textAlign: TextAlign.center),
+ ),
+ ],
+ child: IconButton(
+ onPressed: null,
+ icon: Icon(RemixIcons.user_line),
+ ),
),
],
),
diff --git a/lib/pages/teacher/home/widgets/today_card.dart b/lib/pages/teacher/home/widgets/today_card.dart
index 0edff38..8cf15ff 100644
--- a/lib/pages/teacher/home/widgets/today_card.dart
+++ b/lib/pages/teacher/home/widgets/today_card.dart
@@ -1,127 +1,174 @@
+import 'package:app/pages/teacher/home/viewmodel/home_view_model.dart';
+import 'package:app/request/dto/room/room_info_dto.dart';
import 'package:app/router/route_paths.dart';
+import 'package:app/utils/permission.dart';
import 'package:app/widgets/base/button/index.dart';
import 'package:app/widgets/base/card/g_card.dart';
import 'package:app/widgets/base/config/config.dart';
-import 'package:app/widgets/base/tag/index.dart';
+import 'package:app/widgets/base/empty/index.dart';
import 'package:flutter/material.dart';
+import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:go_router/go_router.dart';
+import 'package:permission_handler/permission_handler.dart';
+import 'package:provider/provider.dart';
import 'package:remixicon/remixicon.dart';
+import 'package:skeletonizer/skeletonizer.dart';
-class TodayCard extends StatelessWidget {
+class TodayCard extends StatefulWidget {
const TodayCard({super.key});
+ @override
+ State createState() => _TodayCardState();
+}
+
+class _TodayCardState extends State {
+ ///前往会议室
+ void _goToRoom() {
+ checkPermission(
+ permissions: [Permission.microphone, Permission.camera],
+ onGranted: () {
+ final vm = context.read();
+ context.push(
+ RoutePaths.tRoom,
+ extra: {
+ "roomId": vm.roomInfo!.id,
+ "startTime": vm.roomInfo!.startTime,
+ },
+ );
+ },
+ onDenied: () {
+ EasyLoading.showError("请开启权限");
+ },
+ onPermanentlyDenied: () {
+ EasyLoading.showError("请手动开启麦克风和摄像头权限");
+ },
+ );
+ }
+
@override
Widget build(BuildContext context) {
- /// item
- Widget item({
- required String title,
- required String value,
- required IconData icon,
- required Color color,
- }) {
- return Expanded(
- child: Container(
- padding: EdgeInsets.all(10),
- decoration: BoxDecoration(
- color: color.withValues(alpha: 0.2),
- borderRadius: BorderRadius.circular(10),
- ),
- child: Row(
- spacing: 10,
- children: [
- Container(
- width: 45,
- height: 45,
- decoration: BoxDecoration(
- color: color,
- borderRadius: BorderRadius.circular(10),
- ),
- child: Icon(
- icon,
- color: Colors.white,
- ),
- ),
- Column(
+ return Consumer(
+ builder: (context, vm, _) {
+ return GCard(
+ child: Visibility(
+ visible: !vm.loading && vm.roomInfo == null,
+ replacement: Skeletonizer(
+ enabled: vm.loading,
+ child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
- Text(title, style: Theme.of(context).textTheme.labelLarge),
- Text(value),
- ],
- ),
- ],
- ),
- ),
- );
- }
-
- return GCard(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Row(
- children: [
- Expanded(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Row(
- spacing: 10,
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ spacing: 10,
+ children: [
+ Skeleton.replace(
+ replacement: Bone.text(),
+ child: Text(vm.roomInfo?.roomName ?? ""),
+ ),
+ ],
+ ),
+ Container(
+ margin: EdgeInsets.only(top: 5),
+ child: Text(
+ "和学生们一起专注学习、共同进步",
+ style: Theme.of(context).textTheme.labelMedium,
+ ),
+ ),
+ ],
+ ),
+ Container(
+ margin: EdgeInsets.only(top: 30),
+ child: Row(
+ spacing: 15,
children: [
- Text("高三数学冲刺班"),
- Tag(text: "待开始"),
+ _item(
+ title: "开始时间",
+ value: vm.roomInfo?.startTime ?? "",
+ icon: RemixIcons.time_line,
+ color: Color(0xff2b7efd),
+ ),
+ _item(
+ title: "结束时间",
+ value: vm.roomInfo?.endTime ?? "",
+ icon: RemixIcons.group_line,
+ color: Color(0xff00c74f),
+ ),
+ _item(
+ title: "时长",
+ value: "${vm.roomMinutes} 分钟",
+ icon: RemixIcons.book_open_line,
+ color: Color(0xffac45fd),
+ ),
],
),
- Container(
- margin: EdgeInsets.only(top: 5),
- child: Text(
- "和学生们一起专注学习、共同进步",
- style: Theme.of(context).textTheme.labelMedium,
- ),
+ ),
+ Container(
+ margin: EdgeInsets.only(top: 30),
+ height: 45,
+ child: Button(
+ text: vm.canEnterRoom ? "开始自习室" : "未到开始时间",
+ type: ThemeType.success,
+ // disabled: !vm.canEnterRoom,
+ onPressed: _goToRoom,
),
- ],
- ),
+ ),
+ ],
),
- Icon(RemixIcons.arrow_right_s_line, size: 30),
- ],
+ ),
+ child: SizedBox(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.center,
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Empty(text: "未分配自习室"),
+ ],
+ ),
+ ),
),
- Container(
- margin: EdgeInsets.only(top: 30),
- child: Row(
- spacing: 15,
+ );
+ },
+ );
+ }
+
+ Widget _item({
+ required String title,
+ required String value,
+ required IconData icon,
+ required Color color,
+ }) {
+ return Expanded(
+ child: Container(
+ padding: EdgeInsets.all(10),
+ decoration: BoxDecoration(
+ color: color.withValues(alpha: 0.2),
+ borderRadius: BorderRadius.circular(10),
+ ),
+ child: Row(
+ spacing: 10,
+ children: [
+ Container(
+ width: 45,
+ height: 45,
+ decoration: BoxDecoration(
+ color: color,
+ borderRadius: BorderRadius.circular(10),
+ ),
+ child: Icon(
+ icon,
+ color: Colors.white,
+ ),
+ ),
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
children: [
- item(
- title: "开始时间",
- value: "14:00",
- icon: RemixIcons.time_line,
- color: Color(0xff2b7efd),
- ),
- item(
- title: "学生人数",
- value: "8 名",
- icon: RemixIcons.group_line,
- color: Color(0xff00c74f),
- ),
- item(
- title: "时长",
- value: "120 分钟",
- icon: RemixIcons.book_open_line,
- color: Color(0xffac45fd),
- ),
+ Text(title, style: Theme.of(context).textTheme.labelLarge),
+ Text(value),
],
),
- ),
- Container(
- margin: EdgeInsets.only(top: 30),
- height: 45,
- child: Button(
- text: "开始自习室",
- type: ThemeType.success,
- onPressed: () {
- context.push(RoutePaths.tRoom);
- },
- ),
- ),
- ],
+ ],
+ ),
),
);
}
diff --git a/lib/pages/teacher/room/t_room_page.dart b/lib/pages/teacher/room/t_room_page.dart
index 2aae604..ee5e23d 100644
--- a/lib/pages/teacher/room/t_room_page.dart
+++ b/lib/pages/teacher/room/t_room_page.dart
@@ -1,12 +1,18 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'controls/top_bar.dart';
-import 'view/student_item.dart';
-import 'view/waiting_start.dart';
+import 'widgets/status_view.dart';
import 'viewmodel/students_view_model.dart';
class TRoomPage extends StatefulWidget {
- const TRoomPage({super.key});
+ final int roomId;
+ final String startTime;
+
+ const TRoomPage({
+ super.key,
+ required this.roomId,
+ required this.startTime,
+ });
@override
State createState() => _TRoomPageState();
@@ -17,37 +23,15 @@ class _TRoomPageState extends State {
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (BuildContext context) {
- return StudentsViewModel();
+ return StudentsViewModel(
+ roomId: widget.roomId,
+ start: widget.startTime,
+ );
},
child: Scaffold(
backgroundColor: Color(0xff2c3032),
appBar: TopBar(),
- body: true
- ? WaitingStart()
- : Padding(
- padding: const EdgeInsets.all(10),
- child: Row(
- spacing: 15,
- children: [
- Expanded(
- child: StudentItem(),
- ),
- SizedBox(
- width: 300,
- child: ListView.separated(
- itemBuilder: (_, index) {
- return SizedBox(
- height: 250,
- child: StudentItem(),
- );
- },
- separatorBuilder: (_, __) => SizedBox(height: 15),
- itemCount: 7,
- ),
- ),
- ],
- ),
- ),
+ body: StatusView(),
),
);
}
diff --git a/lib/pages/teacher/room/view/waiting_start.dart b/lib/pages/teacher/room/view/waiting_start.dart
deleted file mode 100644
index 856292d..0000000
--- a/lib/pages/teacher/room/view/waiting_start.dart
+++ /dev/null
@@ -1,80 +0,0 @@
-import 'dart:async';
-
-import 'package:app/utils/time.dart';
-import 'package:flutter/material.dart';
-
-class WaitingStart extends StatefulWidget {
- const WaitingStart({super.key});
-
- @override
- State createState() => _WaitingStartState();
-}
-
-class _WaitingStartState extends State {
- ///剩余秒
- int _seconds = 0;
- Timer? _timer;
-
- @override
- void initState() {
- super.initState();
- startCountDown();
- }
-
- @override
- void dispose() {
- super.dispose();
- _timer?.cancel();
- _timer = null;
- }
-
- ///开始倒计时
- void startCountDown() {
- //当前时间
- DateTime now = DateTime.now();
- //远端时间
- DateTime remote = DateTime.parse("2025-11-19 17:10:00".replaceFirst(' ', 'T'));
- setState(() {
- _seconds = remote.difference(now).inSeconds;
- });
- _timer = Timer.periodic(Duration(seconds: 1), (timer) {
- setState(() {
- _seconds--;
- });
- if (_seconds <= 0) {
- _timer?.cancel();
- _timer = null;
- _start();
- }
- });
- }
-
- ///倒计时结束开始
- void _start() {}
-
- @override
- Widget build(BuildContext context) {
- return Align(
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- Text(
- "未到开播时间,到点后自动开播",
- style: TextStyle(color: Colors.white),
- ),
- Container(
- margin: EdgeInsets.symmetric(vertical: 10),
- child: Text(
- formatSeconds(_seconds),
- style: TextStyle(
- color: Colors.white,
- fontSize: 26,
- fontWeight: FontWeight.bold,
- ),
- ),
- ),
- ],
- ),
- );
- }
-}
diff --git a/lib/pages/teacher/room/viewmodel/students_view_model.dart b/lib/pages/teacher/room/viewmodel/students_view_model.dart
index f213b6e..2dd3a05 100644
--- a/lib/pages/teacher/room/viewmodel/students_view_model.dart
+++ b/lib/pages/teacher/room/viewmodel/students_view_model.dart
@@ -1,33 +1,78 @@
import 'package:app/data/models/student.dart';
-import 'package:app/websocket/room_websocket.dart';
+import 'package:app/request/dto/room/room_user_dto.dart';
+import 'package:app/request/websocket/room_protocol.dart';
+import 'package:app/request/websocket/room_websocket.dart';
+import 'package:app/utils/time.dart';
import 'package:flutter/cupertino.dart';
class StudentsViewModel extends ChangeNotifier {
///学生摄像头列表
List _students = [];
+ ///房间的基础信息,房间id、房间开始时间
+ final int roomId;
+ late final DateTime startTime;
+
+ StudentsViewModel({required this.roomId, String? start}) {
+ startTime = parseTime(start!);
+ _startRoom();
+ }
+
List get students => _students;
+ ///是否能开始自习室
+ bool get canEnterRoom {
+ final now = DateTime.now();
+
+ // 如果到了开始时间,则可以进入房间
+ if (now.isAfter(startTime)) {
+ return true;
+ }
+ return false;
+ }
+
///websocket管理
late RoomWebSocket _ws;
- StudentsViewModel() {
- _startRoom();
- }
-
///开始链接房间
- void _startRoom() {
+ void _startRoom() async {
_ws = RoomWebSocket();
- _ws.connect();
+ //如果socket的token没有,先初始化
+ if (_ws.wsToken.isEmpty) {
+ await _ws.initToken(roomId);
+ }
+ //启动连接
+ await _ws.connect();
+ //进入房间命令
+ _ws.send(RoomCommand.joinRoom);
+ //监听各种ws事件
_ws.stream.listen((msg) {
- _handleMessage();
+ if (msg.event == RoomEvent.changeUser) {
+ final list = msg.data['user_list'].map((x) => RoomUserDto.fromJson(x)).toList();
+ onStudentChange(list);
+ }
});
notifyListeners();
}
- ///发送命令
- void _handleMessage() {
- print("监听webscoket传来的事件");
+ ///自习室的开关
+ /// - [isOpen]: 是否开启
+ void toggleRoom({required bool isOpen}) {
+ if (isOpen) {
+ _ws.send(RoomCommand.openRoom);
+ } else {
+ _ws.send(RoomCommand.closeRoom);
+ }
+ }
+
+ ///学生人员变化事件,(如加入、退出、掉线)
+ void onStudentChange(List list) {}
+
+ //销毁
+ @override
+ void dispose() {
+ super.dispose();
+ _ws.dispose();
}
}
diff --git a/lib/pages/teacher/room/widgets/content_view.dart b/lib/pages/teacher/room/widgets/content_view.dart
new file mode 100644
index 0000000..aefca41
--- /dev/null
+++ b/lib/pages/teacher/room/widgets/content_view.dart
@@ -0,0 +1,35 @@
+import 'package:flutter/material.dart';
+
+import 'student_item.dart';
+
+class ContentView extends StatelessWidget {
+ const ContentView({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Padding(
+ padding: const EdgeInsets.all(10),
+ child: Row(
+ spacing: 15,
+ children: [
+ Expanded(
+ child: StudentItem(),
+ ),
+ SizedBox(
+ width: 300,
+ child: ListView.separated(
+ itemBuilder: (_, index) {
+ return SizedBox(
+ height: 250,
+ child: StudentItem(),
+ );
+ },
+ separatorBuilder: (_, __) => SizedBox(height: 15),
+ itemCount: 7,
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/pages/teacher/room/widgets/status_view.dart b/lib/pages/teacher/room/widgets/status_view.dart
new file mode 100644
index 0000000..05b64b7
--- /dev/null
+++ b/lib/pages/teacher/room/widgets/status_view.dart
@@ -0,0 +1,114 @@
+import 'dart:async';
+
+import 'package:app/utils/time.dart';
+import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+
+import 'content_view.dart';
+import '../viewmodel/students_view_model.dart';
+
+class StatusView extends StatefulWidget {
+ const StatusView({super.key});
+
+ @override
+ State createState() => _StatusViewState();
+}
+
+class _StatusViewState extends State {
+ ///房间状态
+ RoomStatus status = RoomStatus.loading;
+
+ ///剩余秒
+ int _seconds = 0;
+ Timer? _timer;
+
+ @override
+ void initState() {
+ super.initState();
+ _init();
+ }
+
+ @override
+ void dispose() {
+ super.dispose();
+ _timer?.cancel();
+ _timer = null;
+ }
+
+ void _init() {
+ final vm = context.read();
+ //如果房间可以开始
+ if (vm.canEnterRoom) {
+ status = RoomStatus.start;
+ } else {
+ status = RoomStatus.waiting;
+ startCountDown();
+ }
+ }
+
+ ///开始倒计时
+ void startCountDown() {
+ final vm = context.read();
+ //当前时间
+ DateTime now = DateTime.now();
+ //远端时间
+ setState(() {
+ _seconds = vm.startTime.difference(now).inSeconds;
+ });
+ _timer = Timer.periodic(Duration(seconds: 1), (timer) {
+ setState(() {
+ _seconds--;
+ });
+ if (_seconds <= 0) {
+ _timer?.cancel();
+ _timer = null;
+ setState(() {
+ status = RoomStatus.start;
+ });
+ }
+ });
+ }
+
+ ///开启自习室
+ void openRoom() {
+ final vm = context.read();
+ vm.toggleRoom(isOpen: true);
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ if (status == RoomStatus.waiting) {
+ return Align(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Text(
+ "未到开播时间,到点后自动开播",
+ style: TextStyle(color: Colors.white),
+ ),
+ Container(
+ margin: EdgeInsets.symmetric(vertical: 10),
+ child: Text(
+ formatSeconds(_seconds),
+ style: TextStyle(
+ color: Colors.white,
+ fontSize: 26,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ } else if (status == RoomStatus.start) {
+ return ContentView();
+ }
+ return SizedBox();
+ }
+}
+
+enum RoomStatus {
+ loading, // 加载中
+ waiting, //房间倒计时等待中
+ start, //房间开始中
+}
diff --git a/lib/pages/teacher/room/view/student_item.dart b/lib/pages/teacher/room/widgets/student_item.dart
similarity index 100%
rename from lib/pages/teacher/room/view/student_item.dart
rename to lib/pages/teacher/room/widgets/student_item.dart
diff --git a/lib/providers/user_store.dart b/lib/providers/user_store.dart
index e69de29..881ebe9 100644
--- a/lib/providers/user_store.dart
+++ b/lib/providers/user_store.dart
@@ -0,0 +1,47 @@
+import 'package:app/data/local/storage.dart';
+import 'package:app/request/api/user_api.dart';
+import 'package:app/request/dto/user/user_info_dto.dart';
+import 'package:flutter/cupertino.dart';
+
+class UserStore extends ChangeNotifier {
+ UserInfoDto? userInfo;
+ String token = "";
+
+ ///设置用户数据
+ Future asyncUserInfo() async {
+ if (token.isNotEmpty) {
+ var res = await getUserInfoApi();
+ await Storage.set("user_info", res.toJson());
+ setUserInfo();
+ notifyListeners();
+ }
+ }
+
+ ///获取用户数据
+ Future setUserInfo() async {
+ var info = await Storage.get("user_info");
+ if (info != null) {
+ userInfo = UserInfoDto.fromJson(info);
+ }
+ }
+
+ ///设置token
+ Future setToken(String value) async {
+ token = value;
+ await Storage.set('token', token);
+ }
+
+ ///获取token
+ static Future getToken() async {
+ return await Storage.get("token") ?? '';
+ }
+
+ ///退出登录
+ Future logout() async {
+ logoutApi();
+ await Storage.remove('token');
+ await Storage.remove('user_info');
+ token = '';
+ notifyListeners();
+ }
+}
diff --git a/lib/request/api/room_api.dart b/lib/request/api/room_api.dart
new file mode 100644
index 0000000..46dad16
--- /dev/null
+++ b/lib/request/api/room_api.dart
@@ -0,0 +1,26 @@
+import 'package:app/request/dto/room/rtc_token_dto.dart';
+import 'package:app/request/network/request.dart';
+
+import '../dto/room/room_info_dto.dart';
+
+/// 获取房间列表
+Future> getRoomListApi() async {
+ var res = await Request().get('/study_room/get_study_room_list');
+ return List.from(res.map((x) => RoomInfoDto.fromJson(x)));
+}
+
+///获取自习室的websocket令牌
+Future getWsTokenApi(int roomId) async {
+ var res = await Request().get('/study_room/get_ws_token', {
+ "study_room_id": roomId,
+ });
+ return res['token'];
+}
+
+///获取自习室的RTC令牌
+Future getRtcTokenApi(int roomId) async {
+ var res = await Request().get('/study_room/get_rtc_token', {
+ "study_room_id": roomId,
+ });
+ return RtcTokenDto.fromJson(res);
+}
diff --git a/lib/request/api/user_api.dart b/lib/request/api/user_api.dart
new file mode 100644
index 0000000..f3988e1
--- /dev/null
+++ b/lib/request/api/user_api.dart
@@ -0,0 +1,29 @@
+import 'package:app/request/dto/user/user_info_dto.dart';
+import 'package:app/request/network/request.dart';
+
+import '../dto/user/login_dto.dart';
+
+///发送验证码
+Future sendCodeApi(String tel) async {
+ await Request().get("/send_sms_code", {"tel": tel});
+}
+
+///登录
+Future loginApi(String tel, String code) async {
+ var res = await Request().post("/login", {
+ "tel": tel,
+ "sms_code": code,
+ });
+ return LoginDto.fromJson(res);
+}
+
+/// 获取用户信息
+Future getUserInfoApi() async {
+ var response = await Request().get("/get_my_info");
+ return UserInfoDto.fromJson(response);
+}
+
+///退出登录
+Future logoutApi() async {
+ await Request().get("/logout");
+}
\ No newline at end of file
diff --git a/lib/request/dto/room/room_file_dto.dart b/lib/request/dto/room/room_file_dto.dart
new file mode 100644
index 0000000..3f4d5b2
--- /dev/null
+++ b/lib/request/dto/room/room_file_dto.dart
@@ -0,0 +1,24 @@
+class RoomFileDto {
+ RoomFileDto({
+ this.fileName = "",
+ this.fileSize = 0,
+ this.fileUrl = "",
+ });
+
+ RoomFileDto.fromJson(Map json)
+ : fileName = json['file_name'] ?? "",
+ fileSize = json['file_size'] ?? 0,
+ fileUrl = json['file_url'] ?? "";
+
+ String fileName;
+ int fileSize;
+ String fileUrl;
+
+ Map toJson() {
+ final map = {};
+ map['file_name'] = fileName;
+ map['file_size'] = fileSize;
+ map['file_url'] = fileUrl;
+ return map;
+ }
+}
diff --git a/lib/request/dto/room/room_info_dto.dart b/lib/request/dto/room/room_info_dto.dart
new file mode 100644
index 0000000..8866bfd
--- /dev/null
+++ b/lib/request/dto/room/room_info_dto.dart
@@ -0,0 +1,39 @@
+class RoomInfoDto {
+
+
+ RoomInfoDto({
+ required this.teacherBackground,
+ required this.roomName,
+ required this.startTime,
+ required this.teacherName,
+ required this.endTime,
+ required this.id,
+ });
+
+ String teacherBackground;
+ String roomName;
+ String startTime;
+ String teacherName;
+ String endTime;
+ int id;
+
+ factory RoomInfoDto.fromJson(Map json) =>
+ RoomInfoDto(
+ teacherBackground: json["teacher_background"],
+ roomName: json["room_name"],
+ startTime: json["start_time"],
+ teacherName: json["teacher_name"],
+ endTime: json["end_time"],
+ id: json["id"],
+ );
+
+ Map toJson() =>
+ {
+ "teacher_background": teacherBackground,
+ "room_name": roomName,
+ "start_time": startTime,
+ "teacher_name": teacherName,
+ "end_time": endTime,
+ "id": id,
+ };
+}
diff --git a/lib/request/dto/room/room_type_dto.dart b/lib/request/dto/room/room_type_dto.dart
new file mode 100644
index 0000000..381cd18
--- /dev/null
+++ b/lib/request/dto/room/room_type_dto.dart
@@ -0,0 +1,39 @@
+class RoomTypeDto {
+ final int studyRoomId;
+ final int teacherId;
+ final String teacherRtcUid;
+ final String teacherWsClientId;
+ final int roomStatus;
+ final String dataType;
+
+ RoomTypeDto({
+ required this.studyRoomId,
+ required this.teacherId,
+ required this.teacherRtcUid,
+ required this.teacherWsClientId,
+ required this.roomStatus,
+ required this.dataType,
+ });
+
+ Map toJson() {
+ final map = {};
+ map["study_room_id"] = studyRoomId;
+ map["teacher_id"] = teacherId;
+ map["teacher_rtc_uid"] = teacherRtcUid;
+ map["teacher_ws_client_id"] = teacherWsClientId;
+ map["room_status"] = roomStatus;
+ map["data_type"] = dataType;
+ return map;
+ }
+
+ factory RoomTypeDto.fromJson(Map json) {
+ return RoomTypeDto(
+ studyRoomId: json["study_room_id"] ?? 0,
+ teacherId: json["teacher_id"] ?? 0,
+ teacherRtcUid: json["teacher_rtc_uid"] ?? "",
+ teacherWsClientId: json["teacher_ws_client_id"] ?? "",
+ roomStatus: json["room_status"] ?? 0,
+ dataType: json["data_type"] ?? "",
+ );
+ }
+}
diff --git a/lib/request/dto/room/room_user_dto.dart b/lib/request/dto/room/room_user_dto.dart
new file mode 100644
index 0000000..db221b1
--- /dev/null
+++ b/lib/request/dto/room/room_user_dto.dart
@@ -0,0 +1,67 @@
+class RoomUserDto {
+ final int userId;
+ final String rtcUid;
+ final int microphoneStatus;
+ final int cameraStatus;
+ final int speekerStatus;
+ final String wsClientId;
+ final String userName;
+ final String avatar;
+ final int userType;
+ final List filesList;
+ final String dataType;
+ final int handup;
+ final int online; //0离线,1在线
+
+ const RoomUserDto({
+ required this.userId,
+ required this.rtcUid,
+ required this.microphoneStatus,
+ required this.cameraStatus,
+ required this.speekerStatus,
+ required this.wsClientId,
+ required this.userName,
+ required this.avatar,
+ required this.userType,
+ required this.filesList,
+ required this.dataType,
+ required this.handup,
+ required this.online,
+ });
+
+ factory RoomUserDto.fromJson(Map json) {
+ return RoomUserDto(
+ userId: json["user_id"] ?? 0,
+ rtcUid: json["rtc_uid"] ?? "",
+ microphoneStatus: json["microphone_status"] ?? 0,
+ cameraStatus: json["camera_status"] ?? 0,
+ speekerStatus: json["speeker_status"] ?? 0,
+ wsClientId: json["ws_client_id"] ?? "",
+ userName: json["user_name"] ?? "",
+ avatar: json["avatar"] ?? "",
+ userType: json["user_type"] ?? 0,
+ filesList: json["files"] != null ? List.from(json["files"]) : [],
+ dataType: json["data_type"] ?? "",
+ handup: json["handup"] ?? 0,
+ online: json["online"] ?? 0,
+ );
+ }
+
+ Map toJson() {
+ return {
+ "user_id": userId,
+ "rtc_uid": rtcUid,
+ "microphone_status": microphoneStatus,
+ "camera_status": cameraStatus,
+ "speeker_status": speekerStatus,
+ "ws_client_id": wsClientId,
+ "user_name": userName,
+ "avatar": avatar,
+ "user_type": userType,
+ "files": filesList,
+ "data_type": dataType,
+ "handup": handup,
+ "online": online,
+ };
+ }
+}
diff --git a/lib/request/dto/room/rtc_token_dto.dart b/lib/request/dto/room/rtc_token_dto.dart
new file mode 100644
index 0000000..ee5dfcf
--- /dev/null
+++ b/lib/request/dto/room/rtc_token_dto.dart
@@ -0,0 +1,27 @@
+class RtcTokenDto {
+ RtcTokenDto({
+ required this.uid,
+ required this.expiresAt,
+ required this.channel,
+ required this.token,
+ });
+
+ String uid;
+ DateTime expiresAt;
+ String channel;
+ String token;
+
+ factory RtcTokenDto.fromJson(Map json) => RtcTokenDto(
+ uid: json["uid"],
+ expiresAt: DateTime.parse(json["expires_at"]),
+ channel: json["channel"],
+ token: json["token"],
+ );
+
+ Map toJson() => {
+ "uid": uid,
+ "expires_at": expiresAt.toIso8601String(),
+ "channel": channel,
+ "token": token,
+ };
+}
diff --git a/lib/request/dto/user/login_dto.dart b/lib/request/dto/user/login_dto.dart
new file mode 100644
index 0000000..b6d37ef
--- /dev/null
+++ b/lib/request/dto/user/login_dto.dart
@@ -0,0 +1,14 @@
+class LoginDto {
+ String accessToken;
+
+ LoginDto({required this.accessToken});
+
+ Map toJson() {
+ final map = {};
+ map["accessToken"] = accessToken;
+
+ return map;
+ }
+
+ LoginDto.fromJson(dynamic json) : accessToken = json["accessToken"] ?? "";
+}
diff --git a/lib/request/dto/user/user_info_dto.dart b/lib/request/dto/user/user_info_dto.dart
new file mode 100644
index 0000000..b82feac
--- /dev/null
+++ b/lib/request/dto/user/user_info_dto.dart
@@ -0,0 +1,60 @@
+class UserInfoDto {
+ UserInfoDto({
+ required this.accountType,
+ required this.extraInfo,
+ required this.name,
+ required this.tel,
+ required this.id,
+ required this.avatar,
+ });
+
+ /// 1学生 2老师
+ int accountType;
+ ExtraInfo extraInfo;
+ String name;
+ String tel;
+ int id;
+ String avatar;
+
+ factory UserInfoDto.fromJson(Map json) => UserInfoDto(
+ accountType: json["account_type"],
+ extraInfo: ExtraInfo.fromJson(json["extra_info"]),
+ name: json["name"],
+ tel: json["tel"],
+ id: json["id"],
+ avatar: json["avatar"],
+ );
+
+ Map toJson() => {
+ "account_type": accountType,
+ "extra_info": extraInfo.toJson(),
+ "name": name,
+ "tel": tel,
+ "id": id,
+ "avatar": avatar,
+ };
+}
+
+class ExtraInfo {
+ ExtraInfo({
+ required this.vipEndTime,
+ required this.vipStartTime,
+ required this.vipStatus,
+ });
+
+ String vipEndTime;
+ String vipStartTime;
+ int vipStatus; // 0:普通用户 1:VIP
+
+ factory ExtraInfo.fromJson(Map json) => ExtraInfo(
+ vipEndTime: json["vip_end_time"],
+ vipStartTime: json["vip_start_time"],
+ vipStatus: json["vip_status"],
+ );
+
+ Map toJson() => {
+ "vip_end_time": vipEndTime,
+ "vip_start_time": vipStartTime,
+ "vip_status": vipStatus,
+ };
+}
diff --git a/lib/request/network/interceptor.dart b/lib/request/network/interceptor.dart
index 5e7892e..fae793d 100644
--- a/lib/request/network/interceptor.dart
+++ b/lib/request/network/interceptor.dart
@@ -1,13 +1,16 @@
+import 'package:app/providers/user_store.dart';
import 'package:dio/dio.dart';
+import 'package:flutter_easyloading/flutter_easyloading.dart';
import '../dto/base_dto.dart';
-
///请求拦截器
void onRequest(
RequestOptions options,
RequestInterceptorHandler handler,
-) {
+) async {
+ String token = await UserStore.getToken();
+ options.headers['Authorization'] = 'Bearer $token';
return handler.next(options);
}
@@ -18,6 +21,7 @@ void onResponse(
) {
var apiResponse = ApiDto.fromJson(response.data);
if (apiResponse.code == 1) {
+ response.data = apiResponse.data;
handler.next(response);
} else {
handler.reject(
@@ -35,17 +39,25 @@ void onError(
DioException e,
ErrorInterceptorHandler handler,
) {
+ var title = "";
if (e.type == DioExceptionType.connectionTimeout) {
- print("请求超时");
+ title = "请求超时";
} else if (e.type == DioExceptionType.badResponse) {
if (e.response?.statusCode == 404) {
- print("接口404不存在");
+ title = "接口404不存在";
} else {
- print("500");
+ title = "500";
}
} else if (e.type == DioExceptionType.connectionError) {
- print("网络连接失败");
+ title = "网络连接失败";
} else {
- print("接口请求异常报错");
+ title = "异常其他错误";
}
+ showError(title);
+ handler.next(e);
+}
+
+///显示错误信息
+void showError(String message) {
+ EasyLoading.showError(message);
}
diff --git a/lib/request/websocket/room_protocol.dart b/lib/request/websocket/room_protocol.dart
new file mode 100644
index 0000000..34578c9
--- /dev/null
+++ b/lib/request/websocket/room_protocol.dart
@@ -0,0 +1,103 @@
+enum RoomCommand {
+ ///ping服务器,用于心跳
+ ping("ping"),
+
+ ///加入房间
+ joinRoom("into_room"),
+
+ ///获取房间信息(没啥用)
+ getRoomInfo("room_data"),
+
+ ///学生开关扬声器、摄像头、麦克风
+ switchCamera("mute_self"),
+
+ ///学生上传文件
+ uploadFile("upload_file"),
+
+ ///学生举手
+ handUp("handup"),
+
+ ///老师开启自习室
+ openRoom("start_study_room"),
+
+ ///老师关闭自习室
+ closeRoom("close_study_room"),
+
+ ///老师开关学生的扬声器、关闭摄像头、关闭麦克风
+ switchStudentCamera("mute_user"),
+
+ ///老师清除学生的举手
+ clearHandUp("clear_handup"),
+
+ ///邀请学生进入白板
+ inviteStudent("invite_whiteboard");
+
+ final String value;
+
+ const RoomCommand(this.value);
+}
+
+enum RoomEvent {
+ ///人员变化事件
+ changeUser("sys_room_user_changed"),
+
+ ///学生端开启扬声器
+ openSpeaker("user_unmute_self_speeker"),
+
+ ///学生端关闭扬声器
+ closeSpeaker("user_mute_self_speeker"),
+
+ ///学生开启麦克风
+ openMic("user_unmute_self_microphone"),
+
+ ///学生关闭麦克风
+ closeMic("user_mute_self_microphone"),
+
+ ///学生开启摄像头
+ openCamera("user_unmute_self_camera"),
+
+ ///学生关闭摄像头
+ closeCamera("user_mute_self_camera"),
+
+ ///学生文件上传完毕
+ fileUploadComplete("sys_user_file_uploaded"),
+
+ ///学生举手事件
+ handUp("sys_user_handup"),
+
+ ///自习室以开启,进入自习室(学生用)
+ openRoom("sys_start_study_room"),
+
+ ///自习室以关闭,退出自习室(学生用)
+ closeRoom("sys_close_study_room"),
+
+ ///老师关闭学生的扬声器
+ closeStudentSpeaker("sys_control_mute_speeker"),
+
+ ///老师打开学生的扬声器
+ openStudentSpeaker("sys_control_unmute_speeker"),
+
+ ///老师关闭学生的麦克风
+ closeStudentMic("sys_control_mute_microphone"),
+
+ ///老师关闭学生的摄像头
+ closeStudentCamera("sys_control_mute_camera"),
+
+ ///老师清除学生的举手(学生用)
+ clearHandUp("sys_clear_handup"),
+
+ ///学生收到白板邀请(学生用)
+ inviteWhiteboard("sys_invite_whiteboard");
+
+ final String value;
+
+ 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'),
+ );
+ }
+}
diff --git a/lib/request/websocket/room_websocket.dart b/lib/request/websocket/room_websocket.dart
new file mode 100644
index 0000000..13c38f9
--- /dev/null
+++ b/lib/request/websocket/room_websocket.dart
@@ -0,0 +1,122 @@
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+import 'package:app/config/config.dart';
+import 'package:app/request/api/room_api.dart';
+import 'package:app/request/websocket/room_protocol.dart';
+import 'package:logger/logger.dart';
+
+import '../dto/room/rtc_token_dto.dart';
+
+Logger logger = Logger();
+
+class RoomWebSocket {
+ ///单例设计模式
+ RoomWebSocket._();
+
+ static final RoomWebSocket _instance = RoomWebSocket._();
+
+ factory RoomWebSocket() => _instance;
+
+ /// WebSocket和心跳定时器
+ String url = "";
+ WebSocket? _socket;
+ Timer? _heartbeatTimer;
+ Timer? _reconnectTimer; //错误重连的定时器
+
+ ///令牌
+ String wsToken = ""; //自习室的websocket令牌
+ int roomId = 0; //房间号
+ RtcTokenDto? rtcToken; // rtc的令牌
+
+ ///用 StreamController 分化消息给订阅者
+ final StreamController _msgController = StreamController.broadcast();
+
+ Stream get stream => _msgController.stream;
+
+ ///初始化令牌
+ /// -[id] 房间id
+ Future initToken(int id) async {
+ roomId = id;
+ final rtcFuture = getRtcTokenApi(id);
+ final wsFuture = getWsTokenApi(id);
+
+ rtcToken = await rtcFuture;
+ wsToken = await wsFuture;
+ }
+
+ ///开始连接
+ Future connect() async {
+ try {
+ _socket = await WebSocket.connect(
+ "${Config.wsUrl()}?token=$wsToken&study_room_id=$roomId",
+ );
+ logger.i("连接成功");
+ _reconnectTimer?.cancel();
+ _reconnectTimer = null;
+
+ //监听消息
+ _socket!.listen(
+ (data) {
+ //监听事件
+ final jsonMap = jsonDecode(data);
+ RoomMessage msg = RoomMessage(RoomEvent.fromStr(jsonMap['action']), jsonMap['data']);
+ _msgController.add(msg);
+ },
+ onDone: () {},
+ onError: (_) {
+ logger.e("连接异常断开");
+ },
+ );
+ //心跳
+ _heartbeatTimer?.cancel();
+ _heartbeatTimer = Timer.periodic(Duration(seconds: 15), (_) {
+ logger.i("发送心跳");
+ send(RoomCommand.ping);
+ });
+ } catch (e) {
+ logger.e("连接失败");
+ _reconnect();
+ }
+ }
+
+ ///发送指令
+ void send(RoomCommand action, [Map? params]) {
+ final msg = {
+ "action": action.value,
+ "data": params,
+ };
+ _socket!.add(jsonEncode(msg));
+ }
+
+ ///连接错误事件
+ void _reconnect() {
+ _reconnectTimer?.cancel();
+ _reconnectTimer = Timer.periodic(Duration(seconds: 3), (timer) {
+ logger.e("正在重连");
+ connect();
+ });
+ }
+
+ void dispose() {
+ //心跳取消
+ _heartbeatTimer?.cancel();
+ _heartbeatTimer = null;
+ //socket取消
+ _socket?.close();
+ // 销毁事件流
+ _msgController.close();
+ // 错误重连取消
+ _reconnectTimer?.cancel();
+ _reconnectTimer = null;
+ logger.i("websocket销毁成功");
+ }
+}
+
+///websocket服务器发过来的事件和数据
+class RoomMessage {
+ final RoomEvent event; //事件名称
+ final dynamic data; //事件数据
+
+ RoomMessage(this.event, this.data);
+}
diff --git a/lib/router/modules/teacher_routes.dart b/lib/router/modules/teacher_routes.dart
index 01ca542..a8de2c7 100644
--- a/lib/router/modules/teacher_routes.dart
+++ b/lib/router/modules/teacher_routes.dart
@@ -14,7 +14,13 @@ List teacherRoutes = [
RouterConfig(
path: RoutePaths.tRoom,
child: (state) {
- return TRoomPage();
+ final extra = state.extra as Map?;
+ final roomId = extra?['roomId'] as int?;
+ final startTime = extra?['startTime'] as String?;
+ return TRoomPage(
+ roomId: roomId!,
+ startTime: startTime!,
+ );
},
),
-];
\ No newline at end of file
+];
diff --git a/lib/router/routes.dart b/lib/router/routes.dart
index a242534..cf60c3a 100644
--- a/lib/router/routes.dart
+++ b/lib/router/routes.dart
@@ -23,6 +23,6 @@ List routes = routeConfigs.map((item) {
//变量命名
GoRouter goRouter = GoRouter(
- initialLocation: RoutePaths.tHome,
+ initialLocation: RoutePaths.splash,
routes: routes,
);
diff --git a/lib/utils/permission.dart b/lib/utils/permission.dart
new file mode 100644
index 0000000..1b867a9
--- /dev/null
+++ b/lib/utils/permission.dart
@@ -0,0 +1,65 @@
+import 'dart:ui';
+import 'package:permission_handler/permission_handler.dart';
+
+/// 封装通用权限处理方法
+/// - [permissions] 需要检查的权限列表
+/// - [onGranted] 当所有权限都被授予时调用的回调
+/// - [onDenied] 当有权限被拒绝时调用的回调
+/// - [onPermanentlyDenied] 当有权限被永久拒绝时调用的回调(可选,默认打开设置页)
+Future checkPermission({
+ required List permissions,
+ required VoidCallback onGranted,
+ VoidCallback? onDenied,
+ VoidCallback? onPermanentlyDenied,
+}) async {
+ // 判断当前权限状态
+ Map statuses = {};
+ for (final permission in permissions) {
+ statuses[permission] = await permission.status;
+ }
+
+ // 筛选出未授权的权限
+ final needRequest = statuses.entries
+ .where((entry) => !entry.value.isGranted)
+ .map((entry) => entry.key)
+ .toList();
+ // 如果全部已有权限
+ if (needRequest.isEmpty) {
+ onGranted();
+ return;
+ }
+
+ // 请求未授权的权限
+ final requestResult = await needRequest.request();
+
+ //是否全部授权
+ bool allGranted = true;
+ //是否有任何一个授权了
+ bool anyPermanentlyDenied = false;
+
+ for (final permission in permissions) {
+ final status = requestResult[permission] ?? await permission.status;
+
+ if (status.isPermanentlyDenied) {
+ anyPermanentlyDenied = true;
+ allGranted = false;
+ break;
+ } else if (!status.isGranted) {
+ allGranted = false;
+ }
+ }
+
+ if (allGranted) {
+ onGranted();
+ } else if (anyPermanentlyDenied) {
+ if (onPermanentlyDenied != null) {
+ onPermanentlyDenied();
+ } else {
+ openAppSettings(); // 可选:默认打开设置页
+ }
+ } else {
+ if (onDenied != null) {
+ onDenied();
+ }
+ }
+}
diff --git a/lib/utils/time.dart b/lib/utils/time.dart
index 5d29ad6..e38c72b 100644
--- a/lib/utils/time.dart
+++ b/lib/utils/time.dart
@@ -46,3 +46,16 @@ String formatSeconds(int seconds) {
return '${twoDigits(m)}:${twoDigits(s)}';
}
}
+
+
+/// 将 "HH", "HH:mm" 或 "HH:mm:ss" 转为当天 DateTime
+DateTime parseTime(String timeStr) {
+ final now = DateTime.now();
+ final parts = timeStr.split(':').map(int.parse).toList();
+
+ final hour = parts.length > 0 ? parts[0] : 0;
+ final minute = parts.length > 1 ? parts[1] : 0;
+ final second = parts.length > 2 ? parts[2] : 0;
+
+ return DateTime(now.year, now.month, now.day, hour, minute, second);
+}
diff --git a/lib/websocket/room_websocket.dart b/lib/websocket/room_websocket.dart
deleted file mode 100644
index 1e18775..0000000
--- a/lib/websocket/room_websocket.dart
+++ /dev/null
@@ -1,63 +0,0 @@
-import 'dart:async';
-import 'dart:io';
-import 'package:logger/logger.dart';
-
-Logger logger = Logger();
-
-class RoomWebSocket {
- ///单例设计模式
- RoomWebSocket._();
-
- static final RoomWebSocket _instance = RoomWebSocket._();
-
- factory RoomWebSocket() => _instance;
-
- /// WebSocket和心跳定时器
- String url = "";
- WebSocket? _socket;
- Timer? _heartbeatTimer;
-
- ///用 StreamController 分化消息给订阅者
- final StreamController