自习室优化ok

This commit is contained in:
zhutao
2025-11-28 18:01:09 +08:00
parent 57305c5804
commit 54bf2dcee7
38 changed files with 527 additions and 117 deletions

View File

@@ -20,6 +20,7 @@
<application
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:usesCleartextTraffic="true"
android:label="学光自习室">
<activity
android:name=".MainActivity"

View File

@@ -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
View 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 需要重新登录
// 以后可以扩展其他事件
}

View File

@@ -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(

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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,
);
},
),
),
],
);

View File

@@ -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 {
),
);
}
}
}

View File

@@ -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;

View File

@@ -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(),

View File

@@ -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';

View File

@@ -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) ? '是否关闭所有学生的摄像头?' : '是否关闭所有学生的扬声器?';

View File

@@ -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;

View File

@@ -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,
),
),
),
],

View File

@@ -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);
},
),
],
),
),

View File

@@ -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');
}
}

View File

@@ -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);
}

View 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,
};
}

View File

@@ -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";

View File

@@ -1,5 +1,5 @@
import 'package:dio/dio.dart';
import 'package:app/config/config.dart';
import 'package:app/global/config.dart';
import 'interceptor.dart';

View File

@@ -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';

View File

@@ -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,
);

View File

@@ -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';

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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;

View 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(),
),
),
],
),
),
],
),
);
}
}

View 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),
);
}
}

View File

@@ -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(

View File

@@ -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)),
],
),
),
);
}
}

View File

@@ -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:

View File

@@ -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: