自习室优化ok
This commit is contained in:
@@ -20,6 +20,7 @@
|
||||
<application
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:usesCleartextTraffic="true"
|
||||
android:label="学光自习室">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
|
||||
@@ -19,6 +19,9 @@ class Config {
|
||||
return "wss://xueguang.test.tuzuu.com/ws";
|
||||
}
|
||||
|
||||
///网页域名地址
|
||||
static String get webUrl => "http://xueguang.test.tuzuu.com";
|
||||
|
||||
///声网APPid
|
||||
static String get swAppId => "011c2fd2e1854511a80c1aebded4eee7";
|
||||
}
|
||||
33
lib/global/event_bus.dart
Normal file
33
lib/global/event_bus.dart
Normal file
@@ -0,0 +1,33 @@
|
||||
import 'dart:async';
|
||||
|
||||
/// 全局事件总线
|
||||
class EventBus {
|
||||
EventBus._();
|
||||
|
||||
static final _instance = EventBus._();
|
||||
|
||||
factory EventBus() => _instance;
|
||||
|
||||
|
||||
// StreamController,广播模式可以让多个地方监听
|
||||
final StreamController<GlobalEvent> _controller = StreamController.broadcast();
|
||||
|
||||
/// 发送事件
|
||||
void fire(GlobalEvent event) {
|
||||
_controller.add(event);
|
||||
}
|
||||
|
||||
/// 监听事件
|
||||
Stream<GlobalEvent> get stream => _controller.stream;
|
||||
|
||||
/// 关闭流(一般应用生命周期结束时调用)
|
||||
void dispose() {
|
||||
_controller.close();
|
||||
}
|
||||
}
|
||||
|
||||
/// 事件类型枚举
|
||||
enum GlobalEvent {
|
||||
unauthorized, // 401 需要重新登录
|
||||
// 以后可以扩展其他事件
|
||||
}
|
||||
@@ -1,12 +1,18 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:app/global/event_bus.dart';
|
||||
import 'package:app/providers/user_store.dart';
|
||||
import 'package:app/router/route_paths.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:app/router/routes.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'config/theme/theme.dart';
|
||||
import 'global/theme/theme.dart';
|
||||
import 'global/theme/themes/light_theme.dart';
|
||||
|
||||
void main() {
|
||||
runApp(
|
||||
@@ -19,9 +25,26 @@ void main() {
|
||||
);
|
||||
}
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
class MyApp extends StatefulWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
@override
|
||||
State<MyApp> createState() => _MyAppState();
|
||||
}
|
||||
|
||||
class _MyAppState extends State<MyApp> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// 1. 监听 401
|
||||
EventBus().stream.listen((event) {
|
||||
if (event == GlobalEvent.unauthorized) {
|
||||
final ctx = navigatorKey.currentState?.context;
|
||||
ctx?.go(RoutePaths.login);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ScreenUtilInit(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:app/config/theme/base/app_theme_ext.dart';
|
||||
import 'package:app/global/theme/base/app_theme_ext.dart';
|
||||
import 'package:app/pages/student/home/viewmodel/s_home_vm.dart';
|
||||
import 'package:app/widgets/version/version_dialog.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:app/config/theme/base/app_theme_ext.dart';
|
||||
import 'package:app/global/theme/base/app_theme_ext.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:app/config/theme/base/app_theme_ext.dart';
|
||||
import 'package:app/global/theme/base/app_theme_ext.dart';
|
||||
import 'package:app/pages/student/home/viewmodel/s_home_vm.dart';
|
||||
import 'package:app/router/route_paths.dart';
|
||||
import 'package:app/utils/permission.dart';
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import 'package:app/pages/student/room/viewmodel/stu_room_vm.dart';
|
||||
import 'package:app/providers/user_store.dart';
|
||||
import 'package:app/utils/time.dart';
|
||||
import 'package:app/widgets/base/button/index.dart';
|
||||
import 'package:app/widgets/base/dialog/config_dialog.dart';
|
||||
import 'package:app/widgets/room/board/board_manager.dart';
|
||||
import 'package:app/widgets/room/core/count_down_vm.dart';
|
||||
import 'package:app/widgets/room/other_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
@@ -18,6 +23,8 @@ class TopBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final vm = context.watch<StuRoomVM>();
|
||||
final userStore = context.read<UserStore>();
|
||||
return AppBar(
|
||||
foregroundColor: Colors.white,
|
||||
titleTextStyle: const TextStyle(color: Colors.white, fontSize: 18),
|
||||
@@ -58,16 +65,38 @@ class TopBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
Text(vm.roomInfo!.roomName),
|
||||
Text(
|
||||
formatSeconds(vm.studyTime),
|
||||
style: const TextStyle(fontSize: 12, color: Colors.white24),
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.white24,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: onOther,
|
||||
icon: Icon(showOther ? RemixIcons.team_fill : RemixIcons.team_line),
|
||||
ActionButton(
|
||||
icon: showOther ? RemixIcons.team_fill : RemixIcons.team_line,
|
||||
text: showOther ? "隐藏学生" : '显示学生',
|
||||
onTap: onOther,
|
||||
),
|
||||
Visibility(
|
||||
visible: vm.roomInfo.roomStatus == 1,
|
||||
child: ActionButton(
|
||||
color: Theme.of(context).primaryColor,
|
||||
icon: RemixIcons.artboard_line,
|
||||
text: "进入白板",
|
||||
onTap: () {
|
||||
final boardManager = BoardManager();
|
||||
final vm = context.read<StuRoomVM>();
|
||||
boardManager.showBoardDialog(
|
||||
context,
|
||||
uid: userStore.userInfo!.name,
|
||||
roomId: vm.roomInfo.id,
|
||||
isTeacher: true,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -52,13 +52,13 @@ class TeacherVideo extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
top: 30,
|
||||
left: 10,
|
||||
child: Container(
|
||||
width: 150,
|
||||
width: 200,
|
||||
color: Colors.black,
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1 / 1.2,
|
||||
aspectRatio: 16 / 9,
|
||||
child: AgoraVideoView(
|
||||
controller: VideoViewController(
|
||||
rtcEngine: vm.engine!,
|
||||
@@ -80,4 +80,4 @@ class TeacherVideo extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:agora_rtc_engine/agora_rtc_engine.dart';
|
||||
import 'package:app/config/config.dart';
|
||||
import 'package:app/global/config.dart';
|
||||
import 'package:app/data/models/meeting_room_dto.dart';
|
||||
import 'package:app/request/dto/room/room_list_item_dto.dart';
|
||||
import 'package:app/request/dto/room/room_info_dto.dart';
|
||||
@@ -138,8 +138,9 @@ class StuRoomVM extends ChangeNotifier {
|
||||
|
||||
///学生人员变化事件,(如加入、退出、掉线)
|
||||
void onStudentChange(List<RoomUserDto> list) {
|
||||
final lineList = list.where((t) => t.online == 1);
|
||||
List<RoomUserDto> newList = [];
|
||||
for (var t in list) {
|
||||
for (var t in lineList) {
|
||||
//设置老师
|
||||
if (t.userType == 2) {
|
||||
teacherInfo = t;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:app/config/theme/base/app_theme_ext.dart';
|
||||
import 'package:app/global/theme/base/app_theme_ext.dart';
|
||||
import 'package:app/widgets/version/version_dialog.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
@@ -13,7 +13,7 @@ class THomePage extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
showUpdateDialog(context);
|
||||
// showUpdateDialog(context);
|
||||
return ChangeNotifierProvider(
|
||||
create: (_) => HomeViewModel(),
|
||||
child: const _HomeView(),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:app/config/theme/base/app_theme_ext.dart';
|
||||
import 'package:app/global/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';
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import 'package:app/global/theme/theme.dart';
|
||||
import 'package:app/providers/user_store.dart';
|
||||
import 'package:app/utils/time.dart';
|
||||
import 'package:app/widgets/base/button/index.dart';
|
||||
import 'package:app/widgets/base/config/config.dart';
|
||||
import 'package:app/widgets/base/dialog/config_dialog.dart';
|
||||
import 'package:app/widgets/room/board/board_manager.dart';
|
||||
import 'package:app/widgets/room/other_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
@@ -17,6 +19,7 @@ class TopBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final userStore = context.read<UserStore>();
|
||||
final vm = context.watch<TchRoomVM>();
|
||||
|
||||
return AppBar(
|
||||
@@ -49,28 +52,35 @@ class TopBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
_actionButton(
|
||||
context,
|
||||
ActionButton(
|
||||
icon: RemixIcons.video_on_ai_line,
|
||||
title: "关闭全部",
|
||||
onPressed: () {
|
||||
text: "关闭全部",
|
||||
onTap: () {
|
||||
_closeAll(context, StudentAction.camera);
|
||||
},
|
||||
),
|
||||
_actionButton(
|
||||
context,
|
||||
ActionButton(
|
||||
icon: RemixIcons.volume_up_line,
|
||||
title: "全部静音",
|
||||
onPressed: () {
|
||||
text: "全部静音",
|
||||
onTap: () {
|
||||
_closeAll(context, StudentAction.speaker);
|
||||
},
|
||||
),
|
||||
Container(
|
||||
margin: EdgeInsets.only(right: 15),
|
||||
child: Button(
|
||||
text: "白板",
|
||||
textStyle: TextStyle(fontSize: 14),
|
||||
onPressed: (){},
|
||||
Visibility(
|
||||
visible: vm.roomInfo.roomStatus == 1,
|
||||
child: ActionButton(
|
||||
text: "打开白板",
|
||||
color: Theme.of(context).primaryColor,
|
||||
icon: RemixIcons.artboard_line,
|
||||
onTap: () {
|
||||
final boardManager = BoardManager();
|
||||
boardManager.showBoardDialog(
|
||||
context,
|
||||
uid: userStore.userInfo!.name,
|
||||
roomId: vm.roomInfo.id,
|
||||
isTeacher: true,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Consumer<TchRoomVM>(
|
||||
@@ -78,11 +88,11 @@ class TopBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
if (vm.roomInfo.roomStatus != 1) {
|
||||
return SizedBox();
|
||||
}
|
||||
return Button(
|
||||
type: ThemeType.danger,
|
||||
textStyle: TextStyle(fontSize: 14),
|
||||
return ActionButton(
|
||||
icon: RemixIcons.close_line,
|
||||
color: context.danger,
|
||||
text: "结束自习室",
|
||||
onPressed: () {
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (_) {
|
||||
@@ -103,12 +113,15 @@ class TopBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
);
|
||||
},
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _infoItem(BuildContext context, {required String title, required IconData icon}) {
|
||||
Widget _infoItem(
|
||||
BuildContext context, {
|
||||
required String title,
|
||||
required IconData icon,
|
||||
}) {
|
||||
return Row(
|
||||
children: [
|
||||
Icon(icon, color: Colors.white54, size: 14),
|
||||
@@ -118,32 +131,6 @@ class TopBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _actionButton(
|
||||
BuildContext context, {
|
||||
required IconData icon,
|
||||
required String title,
|
||||
required VoidCallback onPressed,
|
||||
}) {
|
||||
return Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||
margin: EdgeInsets.only(right: 15),
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xff4a4f4f),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: onPressed,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, size: 16),
|
||||
SizedBox(width: 8),
|
||||
Text(title, style: TextStyle(fontSize: 14)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _closeAll(BuildContext context, StudentAction action) {
|
||||
final vm = context.read<TchRoomVM>();
|
||||
String content = (action == StudentAction.camera) ? '是否关闭所有学生的摄像头?' : '是否关闭所有学生的扬声器?';
|
||||
|
||||
@@ -176,7 +176,13 @@ class TchRoomVM extends ChangeNotifier {
|
||||
|
||||
///学生人员变化事件,(如加入、退出、掉线)
|
||||
void onStudentChange(List<RoomUserDto> list) {
|
||||
_students = list.where((t) => t.userType != 2).toList();
|
||||
_students = list.where((t) => t.userType != 2 && t.online == 1).toList();
|
||||
if (activeSId != 0) {
|
||||
final it = _students.where((t) => t.userId == activeSId).firstOrNull;
|
||||
if (it == null) {
|
||||
activeSId = 0;
|
||||
}
|
||||
}
|
||||
// 如果当前没有学生,则选择第一个
|
||||
if (activeSId == 0 && _students.isNotEmpty) {
|
||||
activeSId = _students.first.userId;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:agora_rtc_engine/agora_rtc_engine.dart';
|
||||
import 'package:app/config/config.dart';
|
||||
import 'package:app/global/config.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
@@ -109,13 +109,13 @@ class _ContentViewState extends State<ContentView> {
|
||||
engine: _engine,
|
||||
),
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
top: 10,
|
||||
right: 10,
|
||||
child: Container(
|
||||
width: 150,
|
||||
width: 200,
|
||||
color: Colors.black,
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1 / 1.2,
|
||||
aspectRatio: 16 / 9,
|
||||
child: AgoraVideoView(
|
||||
controller: VideoViewController(
|
||||
rtcEngine: _engine!,
|
||||
@@ -128,21 +128,24 @@ class _ContentViewState extends State<ContentView> {
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 300,
|
||||
child: ListView.separated(
|
||||
itemBuilder: (_, index) {
|
||||
var item = otherStudents.elementAt(index);
|
||||
return SizedBox(
|
||||
height: 250,
|
||||
child: StudentItem(
|
||||
user: item,
|
||||
engine: _engine,
|
||||
),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (_, __) => SizedBox(height: 15),
|
||||
itemCount: otherStudents.length,
|
||||
Visibility(
|
||||
visible: otherStudents.isNotEmpty,
|
||||
child: SizedBox(
|
||||
width: 300,
|
||||
child: ListView.separated(
|
||||
itemBuilder: (_, index) {
|
||||
var item = otherStudents.elementAt(index);
|
||||
return SizedBox(
|
||||
height: 250,
|
||||
child: StudentItem(
|
||||
user: item,
|
||||
engine: _engine,
|
||||
),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (_, __) => SizedBox(height: 15),
|
||||
itemCount: otherStudents.length,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -26,9 +26,10 @@ class StudentItem extends StatefulWidget {
|
||||
|
||||
class _StudentItemState extends State<StudentItem> {
|
||||
///打开文件列表
|
||||
void _openFileList() {
|
||||
void _openFileList({required String name}) {
|
||||
showFileDialog(
|
||||
context,
|
||||
name: name,
|
||||
isUpload: false,
|
||||
files: widget.user.filesList,
|
||||
);
|
||||
@@ -160,7 +161,12 @@ class _StudentItemState extends State<StudentItem> {
|
||||
);
|
||||
},
|
||||
),
|
||||
_actionItem(icon: RemixIcons.file_list_3_fill, onTap: _openFileList),
|
||||
_actionItem(
|
||||
icon: RemixIcons.file_list_3_fill,
|
||||
onTap: (){
|
||||
_openFileList(name: widget.user.userName);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -7,9 +7,9 @@ class UserStore extends ChangeNotifier {
|
||||
UserInfoDto? userInfo;
|
||||
String token = "";
|
||||
|
||||
Future<void> init() async{
|
||||
Future<void> init() async {
|
||||
token = await getToken();
|
||||
await setUserInfo();
|
||||
await setUserInfo();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -41,4 +41,10 @@ class UserStore extends ChangeNotifier {
|
||||
token = '';
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
///强制退出(不调用接口、不通知 UI)
|
||||
static Future<void> forceLogout() async {
|
||||
await Storage.remove('token');
|
||||
await Storage.remove('user_info');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import 'package:app/request/dto/room/board_token_dto.dart';
|
||||
import 'package:app/request/dto/room/rtc_token_dto.dart';
|
||||
import 'package:app/request/network/request.dart';
|
||||
|
||||
import '../dto/room/room_list_item_dto.dart';
|
||||
|
||||
/// 获取房间列表
|
||||
Future<List<RoomListItemDto >> getRoomListApi() async {
|
||||
Future<List<RoomListItemDto>> getRoomListApi() async {
|
||||
var res = await Request().get('/study_room/get_study_room_list');
|
||||
return List<RoomListItemDto >.from(res.map((x) => RoomListItemDto .fromJson(x)));
|
||||
return List<RoomListItemDto>.from(res.map((x) => RoomListItemDto.fromJson(x)));
|
||||
}
|
||||
|
||||
///获取自习室的websocket令牌
|
||||
@@ -24,3 +25,11 @@ Future<RtcTokenDto> getRtcTokenApi(int roomId) async {
|
||||
});
|
||||
return RtcTokenDto.fromJson(res);
|
||||
}
|
||||
|
||||
///获取白板的令牌
|
||||
Future<BoardTokenDto> getBoardTokenApi(int roomId) async {
|
||||
var res = await Request().get('/study_room/get_whiteboard_token', {
|
||||
"study_room_id": roomId,
|
||||
});
|
||||
return BoardTokenDto.fromJson(res);
|
||||
}
|
||||
|
||||
32
lib/request/dto/room/board_token_dto.dart
Normal file
32
lib/request/dto/room/board_token_dto.dart
Normal file
@@ -0,0 +1,32 @@
|
||||
|
||||
class BoardTokenDto {
|
||||
BoardTokenDto({
|
||||
required this.expiresAt,
|
||||
required this.whiteboardAppid,
|
||||
required this.whiteboardUuid,
|
||||
required this.expiresIn,
|
||||
required this.whiteboardToken,
|
||||
});
|
||||
|
||||
DateTime expiresAt;
|
||||
String whiteboardAppid;
|
||||
String whiteboardUuid;
|
||||
int expiresIn;
|
||||
String whiteboardToken;
|
||||
|
||||
factory BoardTokenDto.fromJson(Map<dynamic, dynamic> json) => BoardTokenDto(
|
||||
expiresAt: DateTime.parse(json["expires_at"]),
|
||||
whiteboardAppid: json["whiteboard_appid"],
|
||||
whiteboardUuid: json["whiteboard_uuid"],
|
||||
expiresIn: json["expires_in"],
|
||||
whiteboardToken: json["whiteboard_token"],
|
||||
);
|
||||
|
||||
Map<dynamic, dynamic> toJson() => {
|
||||
"expires_at": expiresAt.toIso8601String(),
|
||||
"whiteboard_appid": whiteboardAppid,
|
||||
"whiteboard_uuid": whiteboardUuid,
|
||||
"expires_in": expiresIn,
|
||||
"whiteboard_token": whiteboardToken,
|
||||
};
|
||||
}
|
||||
@@ -1,34 +1,46 @@
|
||||
import 'package:app/global/event_bus.dart';
|
||||
import 'package:app/providers/user_store.dart';
|
||||
import 'package:app/router/route_paths.dart';
|
||||
import 'package:app/router/routes.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../dto/base_dto.dart';
|
||||
|
||||
///请求拦截器
|
||||
void onRequest(
|
||||
RequestOptions options,
|
||||
RequestInterceptorHandler handler,
|
||||
) async {
|
||||
void onRequest(RequestOptions options,
|
||||
RequestInterceptorHandler handler,) async {
|
||||
String token = await UserStore.getToken();
|
||||
options.headers['Authorization'] = 'Bearer $token';
|
||||
return handler.next(options);
|
||||
}
|
||||
|
||||
///响应拦截器
|
||||
void onResponse(
|
||||
Response<dynamic> response,
|
||||
ResponseInterceptorHandler handler,
|
||||
) {
|
||||
void onResponse(Response<dynamic> response,
|
||||
ResponseInterceptorHandler handler,) async {
|
||||
var apiResponse = ApiDto.fromJson(response.data);
|
||||
if (apiResponse.code == 1) {
|
||||
response.data = apiResponse.data;
|
||||
handler.next(response);
|
||||
} else if (apiResponse.code == 401) {
|
||||
// final bus = EventBus();
|
||||
// bus.fire(GlobalEvent.unauthorized);
|
||||
// final context = navigatorKey.currentState?.context;
|
||||
// print("dsd");
|
||||
// if (context != null) {
|
||||
// // UserStore userStore = context.read<UserStore>();
|
||||
// // userStore.logout();
|
||||
// context.go(RoutePaths.login);
|
||||
// }
|
||||
handler.next(response);
|
||||
} else {
|
||||
handler.reject(
|
||||
DioException(
|
||||
requestOptions: response.requestOptions,
|
||||
response: response,
|
||||
error: {'code': 0, 'message': apiResponse.message},
|
||||
error: {'code': apiResponse.code, 'message': apiResponse.message},
|
||||
),
|
||||
);
|
||||
showError(apiResponse.message);
|
||||
@@ -36,15 +48,18 @@ void onResponse(
|
||||
}
|
||||
|
||||
///错误响应
|
||||
void onError(
|
||||
DioException e,
|
||||
ErrorInterceptorHandler handler,
|
||||
) {
|
||||
void onError(DioException e,
|
||||
ErrorInterceptorHandler handler,) async {
|
||||
var title = "";
|
||||
if (e.type == DioExceptionType.connectionTimeout) {
|
||||
title = "请求超时";
|
||||
} else if (e.type == DioExceptionType.badResponse) {
|
||||
if (e.response?.statusCode == 404) {
|
||||
if (e.response?.statusCode == 401) {
|
||||
final bus = EventBus();
|
||||
bus.fire(GlobalEvent.unauthorized);
|
||||
title = "登录信息已失效,请重新登录";
|
||||
}
|
||||
else if (e.response?.statusCode == 404) {
|
||||
title = "接口404不存在";
|
||||
} else {
|
||||
title = "500";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:app/config/config.dart';
|
||||
import 'package:app/global/config.dart';
|
||||
|
||||
import 'interceptor.dart';
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:app/config/config.dart';
|
||||
import 'package:app/global/config.dart';
|
||||
import 'package:app/request/api/room_api.dart';
|
||||
import 'package:app/request/websocket/room_protocol.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:app/router/modules/student_routes.dart';
|
||||
import 'package:app/router/modules/teacher_routes.dart';
|
||||
import 'package:app/router/route_paths.dart';
|
||||
import 'package:app/router/router_config.dart';
|
||||
import 'package:flutter/material.dart' hide RouterConfig;
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
List<RouterConfig> routeConfigs = [
|
||||
@@ -21,8 +22,12 @@ List<RouteBase> routes = routeConfigs.map((item) {
|
||||
);
|
||||
}).toList();
|
||||
|
||||
GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
|
||||
//变量命名
|
||||
GoRouter goRouter = GoRouter(
|
||||
navigatorKey: navigatorKey,
|
||||
initialLocation: RoutePaths.splash,
|
||||
routes: routes,
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:app/config/config.dart';
|
||||
import 'package:app/global/config.dart';
|
||||
import 'package:app/request/api/common_api.dart';
|
||||
import 'package:app/request/dto/common/qiu_token_dto.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import 'package:app/config/theme/theme.dart';
|
||||
import 'package:app/global/theme/theme.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../config/config.dart';
|
||||
|
||||
|
||||
class Button extends StatelessWidget {
|
||||
final double? width;
|
||||
final String text;
|
||||
@@ -12,14 +13,16 @@ class Button extends StatelessWidget {
|
||||
final VoidCallback? onPressed;
|
||||
final bool loading;
|
||||
final bool disabled;
|
||||
final Widget? icon;
|
||||
|
||||
const Button({
|
||||
super.key,
|
||||
this.icon,
|
||||
this.width,
|
||||
this.textStyle = const TextStyle(),
|
||||
this.radius = const BorderRadius.all(Radius.circular(80)),
|
||||
required this.text,
|
||||
this.onPressed,
|
||||
this.onPressed,
|
||||
this.type = ThemeType.primary,
|
||||
this.loading = false,
|
||||
this.disabled = false,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:app/config/theme/base/app_theme_ext.dart';
|
||||
import 'package:app/global/theme/base/app_theme_ext.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class GCard extends StatelessWidget {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import 'package:app/config/theme/base/app_theme_ext.dart';
|
||||
import 'package:app/global/theme/base/app_theme_ext.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../config/color.dart';
|
||||
import '../config/config.dart';
|
||||
|
||||
|
||||
class Tag extends StatelessWidget {
|
||||
final String text;
|
||||
final Color? color;
|
||||
|
||||
73
lib/widgets/room/board/board_dialog.dart
Normal file
73
lib/widgets/room/board/board_dialog.dart
Normal file
@@ -0,0 +1,73 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:remixicon/remixicon.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
|
||||
|
||||
class BoardDialog extends StatefulWidget {
|
||||
final WebViewController controller;
|
||||
const BoardDialog({super.key, required this.controller});
|
||||
|
||||
@override
|
||||
State<BoardDialog> createState() => _BoardDialogState();
|
||||
}
|
||||
|
||||
class _BoardDialogState extends State<BoardDialog> {
|
||||
bool _loading = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// 绑定加载事件
|
||||
widget.controller.setNavigationDelegate(
|
||||
NavigationDelegate(
|
||||
onPageStarted: (_) {
|
||||
setState(() => _loading = true);
|
||||
},
|
||||
onPageFinished: (_) {
|
||||
setState(() => _loading = false);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
color: Colors.white,
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('白板', style: Theme.of(context).textTheme.titleSmall),
|
||||
IconButton(
|
||||
onPressed: () => context.pop(),
|
||||
icon: const Icon(RemixIcons.close_circle_fill),
|
||||
),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: Stack(
|
||||
children: [
|
||||
WebViewWidget(
|
||||
controller: widget.controller,
|
||||
),
|
||||
if (_loading)
|
||||
Container(
|
||||
color: Colors.white,
|
||||
child: const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
101
lib/widgets/room/board/board_manager.dart
Normal file
101
lib/widgets/room/board/board_manager.dart
Normal file
@@ -0,0 +1,101 @@
|
||||
import 'package:app/global/config.dart';
|
||||
import 'package:app/request/api/room_api.dart';
|
||||
import 'package:app/request/dto/room/board_token_dto.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
|
||||
import 'board_dialog.dart';
|
||||
|
||||
class BoardManager {
|
||||
BoardManager._();
|
||||
|
||||
static final BoardManager _instance = BoardManager._();
|
||||
|
||||
factory BoardManager() => _instance;
|
||||
|
||||
WebViewController? _webViewController;
|
||||
BoardTokenDto? _boardToken;
|
||||
|
||||
///获取token
|
||||
Future<void> _fetchToken(int roomId) async {
|
||||
final now = DateTime.now();
|
||||
|
||||
if (_boardToken == null) {
|
||||
EasyLoading.show(status: "开启白板中");
|
||||
_boardToken = await getBoardTokenApi(roomId);
|
||||
} else {
|
||||
//离过期差多少小时
|
||||
int remainSeconds = _boardToken!.expiresAt.difference(now).inSeconds;
|
||||
// 剩余不足 2 小时 = 7200 秒
|
||||
if (remainSeconds <= 7200) {
|
||||
EasyLoading.show(status: "开启白板中");
|
||||
_boardToken = await getBoardTokenApi(roomId);
|
||||
}
|
||||
}
|
||||
EasyLoading.dismiss();
|
||||
}
|
||||
|
||||
///生成完整 URL
|
||||
/// -[uid] 用户
|
||||
/// -[roomId] 房间id
|
||||
/// -[isTeacher] 是否是老师
|
||||
Future<String> buildUrl({
|
||||
required String uid,
|
||||
required int roomId,
|
||||
required bool isTeacher,
|
||||
}) async {
|
||||
await _fetchToken(roomId);
|
||||
|
||||
Map<String, dynamic> params = {
|
||||
"appid": _boardToken!.whiteboardAppid,
|
||||
"uid": uid,
|
||||
"uuid": _boardToken!.whiteboardUuid,
|
||||
"room_token": _boardToken!.whiteboardToken,
|
||||
"write": isTeacher ? 1 : 0,
|
||||
};
|
||||
String paramStr = params.entries.map((e) => "${e.key}=${e.value}").join("&");
|
||||
|
||||
return "${Config.webUrl}/board?$paramStr";
|
||||
}
|
||||
|
||||
/// 获取全局唯一 WebViewController
|
||||
Future<WebViewController> getController(String url) async {
|
||||
if (_webViewController != null) return _webViewController!;
|
||||
|
||||
_webViewController = WebViewController()
|
||||
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
||||
..setNavigationDelegate(
|
||||
// ⭐ 加上加载监听(BoardDialog 再 listen 一次也没关系)
|
||||
NavigationDelegate(
|
||||
onPageStarted: (_) {},
|
||||
onPageFinished: (_) {},
|
||||
),
|
||||
)
|
||||
..loadRequest(Uri.parse(url));
|
||||
|
||||
return _webViewController!;
|
||||
}
|
||||
|
||||
///打开白板
|
||||
/// -[uid] 用户名+id
|
||||
/// -[roomId] 房间id
|
||||
/// -[isTeacher] 是否是老师
|
||||
Future<void> showBoardDialog(
|
||||
BuildContext context, {
|
||||
required String uid,
|
||||
required int roomId,
|
||||
required bool isTeacher,
|
||||
}) async {
|
||||
String url = await buildUrl(
|
||||
uid: uid,
|
||||
roomId: roomId,
|
||||
isTeacher: isTeacher,
|
||||
);
|
||||
final controller = await getController(url);
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (_) => BoardDialog(controller: controller),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:app/config/theme/base/app_theme_ext.dart';
|
||||
import 'package:app/global/theme/base/app_theme_ext.dart';
|
||||
import 'package:app/utils/transfer/upload.dart';
|
||||
import 'package:app/widgets/base/actionSheet/action_sheet.dart';
|
||||
import 'package:app/widgets/base/actionSheet/type.dart';
|
||||
@@ -150,7 +150,7 @@ class _FileDrawerState extends State<FileDrawer> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"${widget.name ?? ""}上传文件列表",
|
||||
"${widget.name ?? ""}文件列表",
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
Expanded(
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// 举手按钮
|
||||
class HandRaiseButton extends StatelessWidget {
|
||||
final void Function() onTap;
|
||||
|
||||
const HandRaiseButton({super.key, required this.onTap});
|
||||
|
||||
@override
|
||||
@@ -24,3 +26,41 @@ class HandRaiseButton extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
///topBar的操作按钮
|
||||
class ActionButton extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String text;
|
||||
final Color? color;
|
||||
final void Function()? onTap;
|
||||
|
||||
const ActionButton({
|
||||
super.key,
|
||||
required this.icon,
|
||||
required this.text,
|
||||
this.color,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||
margin: EdgeInsets.only(right: 15),
|
||||
decoration: BoxDecoration(
|
||||
color: color ?? Color(0xff4a4f4f),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, size: 16),
|
||||
SizedBox(width: 8),
|
||||
Text(text, style: TextStyle(fontSize: 14)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
32
pubspec.lock
32
pubspec.lock
@@ -898,6 +898,38 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
webview_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: webview_flutter
|
||||
sha256: c3e4fe614b1c814950ad07186007eff2f2e5dd2935eba7b9a9a1af8e5885f1ba
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.13.0"
|
||||
webview_flutter_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webview_flutter_android
|
||||
sha256: "9a25f6b4313978ba1c2cda03a242eea17848174912cfb4d2d8ee84a556f248e3"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.10.1"
|
||||
webview_flutter_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webview_flutter_platform_interface
|
||||
sha256: "63d26ee3aca7256a83ccb576a50272edd7cfc80573a4305caa98985feb493ee0"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.14.0"
|
||||
webview_flutter_wkwebview:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webview_flutter_wkwebview
|
||||
sha256: fb46db8216131a3e55bcf44040ca808423539bc6732e7ed34fb6d8044e3d512f
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.23.0"
|
||||
win32:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -33,6 +33,7 @@ dependencies:
|
||||
app_installer: ^1.3.1
|
||||
wakelock_plus: ^1.3.3
|
||||
image_picker: ^1.2.0
|
||||
webview_flutter: ^4.13.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
Reference in New Issue
Block a user