This commit is contained in:
zhutao
2025-11-20 18:00:34 +08:00
parent 701b99b138
commit b7239292d1
45 changed files with 1499 additions and 354 deletions

View File

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

View File

@@ -1,28 +1,45 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 网络权限-->
<uses-permission android:name="android.permission.INTERNET" />
<!-- 读取手机状态,如电话是不是打进来-->
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<!-- 录音权限,采集声音-->
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<!-- 摄像头权限,采集视频-->
<uses-permission android:name="android.permission.CAMERA" />
<!-- 修改系统音频路由,比如切换扬声器、耳机、调整音频模式-->
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<!-- 查看 WiFi 状态,比如当前是否连着 WiFi-->
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<!-- 查看网络状态,比如是否连网-->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- 安装包权限 -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<application
android:label="xueguang_flutter_app"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
android:icon="@mipmap/ic_launcher"
android:label="学光自习室">
<activity
android:name=".MainActivity"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:exported="true"
android:hardwareAccelerated="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" />
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
@@ -38,8 +55,8 @@
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
<action android:name="android.intent.action.PROCESS_TEXT" />
<data android:mimeType="text/plain" />
</intent>
</queries>
</manifest>

View File

@@ -1,4 +1,4 @@
package com.zkwl.xueguang.xueguang_flutter_app
package com.zkwl.xueguang
import io.flutter.embedding.android.FlutterActivity

BIN
assets/image/empty_data.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 KiB

3
devtools_options.yaml Normal file
View File

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

View File

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

View File

@@ -0,0 +1,50 @@
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
class Storage {
//存储数据
static Future<void> 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<dynamic> 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<void> remove(key) async {
SharedPreferences sp = await SharedPreferences.getInstance();
sp.remove(key);
}
//判断键是否存在
static Future<bool> hasKey(String key) async {
SharedPreferences sp = await SharedPreferences.getInstance();
return sp.containsKey(key);
}
}

View File

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

View File

@@ -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<LoginPage> {
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<LoginPage> {
EasyLoading.showToast("请填写正确的手机号");
return;
}
sendCodeApi(_telController.text);
setState(() {
_countDown = 60;
});
@@ -63,7 +69,29 @@ class _LoginPageState extends State<LoginPage> {
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<UserStore>();
//设置登录信息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<LoginPage> {
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!;
// });
// },
// ),
// ),
],
),
),

View File

@@ -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<SplashPage> {
///权限效验初始化
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>();
userStore.setUserInfo();
//去学生主页
if (userStore.userInfo?.accountType == 1) {
context.go(RoutePaths.sHome);
} else {
context.go(RoutePaths.tHome);
}
print("执行用户数据同步了");
userStore.asyncUserInfo();
}
}
});
}

View File

@@ -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<SHomePage> {
///刷新状态
Future<void> _refresh() async {
await Future.delayed(Duration(seconds: 1));

View File

@@ -16,7 +16,6 @@ class STodayCard extends StatefulWidget {
}
class _STodayCardState extends State<STodayCard> {
///进入自习室
void _handleEnterRoom() {
context.push(RoutePaths.sRoom);

View File

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

View File

@@ -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<THomePage> createState() => _THomePageState();
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => HomeViewModel(),
child: const _HomeView(),
);
}
}
class _THomePageState extends State<THomePage> {
class _HomeView extends StatelessWidget {
const _HomeView();
@override
Widget build(BuildContext context) {
final vm = context.read<HomeViewModel>();
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(),
],
),
),
);
}

View File

@@ -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<void> loadData() async {
final list = await getRoomListApi();
loading = false;
if (list.isNotEmpty) {
roomInfo = list.first;
}
notifyListeners();
}
///计算会议时间
int get roomMinutes {
if (roomInfo == null) return 0;
final start = 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;
}
}

View File

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

View File

@@ -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<TodayCard> createState() => _TodayCardState();
}
class _TodayCardState extends State<TodayCard> {
///前往会议室
void _goToRoom() {
checkPermission(
permissions: [Permission.microphone, Permission.camera],
onGranted: () {
final vm = context.read<HomeViewModel>();
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<HomeViewModel>(
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);
},
),
),
],
],
),
),
);
}

View File

@@ -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<TRoomPage> createState() => _TRoomPageState();
@@ -17,37 +23,15 @@ class _TRoomPageState extends State<TRoomPage> {
Widget build(BuildContext context) {
return ChangeNotifierProvider<StudentsViewModel>(
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(),
),
);
}

View File

@@ -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<WaitingStart> createState() => _WaitingStartState();
}
class _WaitingStartState extends State<WaitingStart> {
///剩余秒
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,
),
),
),
],
),
);
}
}

View File

@@ -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<Student> _students = [];
///房间的基础信息房间id、房间开始时间
final int roomId;
late final DateTime startTime;
StudentsViewModel({required this.roomId, String? start}) {
startTime = parseTime(start!);
_startRoom();
}
List<Student> 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<RoomUserDto> list) {}
//销毁
@override
void dispose() {
super.dispose();
_ws.dispose();
}
}

View File

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

View File

@@ -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<StatusView> createState() => _StatusViewState();
}
class _StatusViewState extends State<StatusView> {
///房间状态
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<StudentsViewModel>();
//如果房间可以开始
if (vm.canEnterRoom) {
status = RoomStatus.start;
} else {
status = RoomStatus.waiting;
startCountDown();
}
}
///开始倒计时
void startCountDown() {
final vm = context.read<StudentsViewModel>();
//当前时间
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<StudentsViewModel>();
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, //房间开始中
}

View File

@@ -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<void> asyncUserInfo() async {
if (token.isNotEmpty) {
var res = await getUserInfoApi();
await Storage.set("user_info", res.toJson());
setUserInfo();
notifyListeners();
}
}
///获取用户数据
Future<void> setUserInfo() async {
var info = await Storage.get("user_info");
if (info != null) {
userInfo = UserInfoDto.fromJson(info);
}
}
///设置token
Future<void> setToken(String value) async {
token = value;
await Storage.set('token', token);
}
///获取token
static Future<String> getToken() async {
return await Storage.get("token") ?? '';
}
///退出登录
Future<void> logout() async {
logoutApi();
await Storage.remove('token');
await Storage.remove('user_info');
token = '';
notifyListeners();
}
}

View File

@@ -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<List<RoomInfoDto>> getRoomListApi() async {
var res = await Request().get('/study_room/get_study_room_list');
return List<RoomInfoDto>.from(res.map((x) => RoomInfoDto.fromJson(x)));
}
///获取自习室的websocket令牌
Future<String> getWsTokenApi(int roomId) async {
var res = await Request().get('/study_room/get_ws_token', {
"study_room_id": roomId,
});
return res['token'];
}
///获取自习室的RTC令牌
Future<RtcTokenDto> getRtcTokenApi(int roomId) async {
var res = await Request().get('/study_room/get_rtc_token', {
"study_room_id": roomId,
});
return RtcTokenDto.fromJson(res);
}

View File

@@ -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<void> sendCodeApi(String tel) async {
await Request().get("/send_sms_code", {"tel": tel});
}
///登录
Future<LoginDto> loginApi(String tel, String code) async {
var res = await Request().post("/login", {
"tel": tel,
"sms_code": code,
});
return LoginDto.fromJson(res);
}
/// 获取用户信息
Future<UserInfoDto> getUserInfoApi() async {
var response = await Request().get("/get_my_info");
return UserInfoDto.fromJson(response);
}
///退出登录
Future<void> logoutApi() async {
await Request().get("/logout");
}

View File

@@ -0,0 +1,24 @@
class RoomFileDto {
RoomFileDto({
this.fileName = "",
this.fileSize = 0,
this.fileUrl = "",
});
RoomFileDto.fromJson(Map<String, dynamic> json)
: fileName = json['file_name'] ?? "",
fileSize = json['file_size'] ?? 0,
fileUrl = json['file_url'] ?? "";
String fileName;
int fileSize;
String fileUrl;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['file_name'] = fileName;
map['file_size'] = fileSize;
map['file_url'] = fileUrl;
return map;
}
}

View File

@@ -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<dynamic, dynamic> 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<dynamic, dynamic> toJson() =>
{
"teacher_background": teacherBackground,
"room_name": roomName,
"start_time": startTime,
"teacher_name": teacherName,
"end_time": endTime,
"id": id,
};
}

View File

@@ -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<String, dynamic> toJson() {
final map = <String, dynamic>{};
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<String, dynamic> 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"] ?? "",
);
}
}

View File

@@ -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<String> 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<String, dynamic> 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<String>.from(json["files"]) : <String>[],
dataType: json["data_type"] ?? "",
handup: json["handup"] ?? 0,
online: json["online"] ?? 0,
);
}
Map<String, dynamic> 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,
};
}
}

View File

@@ -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<dynamic, dynamic> json) => RtcTokenDto(
uid: json["uid"],
expiresAt: DateTime.parse(json["expires_at"]),
channel: json["channel"],
token: json["token"],
);
Map<dynamic, dynamic> toJson() => {
"uid": uid,
"expires_at": expiresAt.toIso8601String(),
"channel": channel,
"token": token,
};
}

View File

@@ -0,0 +1,14 @@
class LoginDto {
String accessToken;
LoginDto({required this.accessToken});
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map["accessToken"] = accessToken;
return map;
}
LoginDto.fromJson(dynamic json) : accessToken = json["accessToken"] ?? "";
}

View File

@@ -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<dynamic, dynamic> 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<dynamic, dynamic> 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<dynamic, dynamic> json) => ExtraInfo(
vipEndTime: json["vip_end_time"],
vipStartTime: json["vip_start_time"],
vipStatus: json["vip_status"],
);
Map<dynamic, dynamic> toJson() => {
"vip_end_time": vipEndTime,
"vip_start_time": vipStartTime,
"vip_status": vipStatus,
};
}

View File

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

View File

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

View File

@@ -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<RoomMessage> _msgController = StreamController.broadcast();
Stream<RoomMessage> get stream => _msgController.stream;
///初始化令牌
/// -[id] 房间id
Future<void> initToken(int id) async {
roomId = id;
final rtcFuture = getRtcTokenApi(id);
final wsFuture = getWsTokenApi(id);
rtcToken = await rtcFuture;
wsToken = await wsFuture;
}
///开始连接
Future<void> 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<String, dynamic>? 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);
}

View File

@@ -14,7 +14,13 @@ List<RouterConfig> teacherRoutes = [
RouterConfig(
path: RoutePaths.tRoom,
child: (state) {
return TRoomPage();
final extra = state.extra as Map<String, dynamic>?;
final roomId = extra?['roomId'] as int?;
final startTime = extra?['startTime'] as String?;
return TRoomPage(
roomId: roomId!,
startTime: startTime!,
);
},
),
];
];

View File

@@ -23,6 +23,6 @@ List<RouteBase> routes = routeConfigs.map((item) {
//变量命名
GoRouter goRouter = GoRouter(
initialLocation: RoutePaths.tHome,
initialLocation: RoutePaths.splash,
routes: routes,
);

65
lib/utils/permission.dart Normal file
View File

@@ -0,0 +1,65 @@
import 'dart:ui';
import 'package:permission_handler/permission_handler.dart';
/// 封装通用权限处理方法
/// - [permissions] 需要检查的权限列表
/// - [onGranted] 当所有权限都被授予时调用的回调
/// - [onDenied] 当有权限被拒绝时调用的回调
/// - [onPermanentlyDenied] 当有权限被永久拒绝时调用的回调(可选,默认打开设置页)
Future<void> checkPermission({
required List<Permission> permissions,
required VoidCallback onGranted,
VoidCallback? onDenied,
VoidCallback? onPermanentlyDenied,
}) async {
// 判断当前权限状态
Map<Permission, PermissionStatus> 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();
}
}
}

View File

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

View File

@@ -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<Map<String, dynamic>> _msgController = StreamController.broadcast();
Stream<Map<String, dynamic>> get stream => _msgController.stream;
///开始连接
Future<void> connect() async {
try {
_socket = await WebSocket.connect(url);
//监听消息
_socket!.listen(
(data) {},
onDone: () {},
onError: (_) {
logger.e("连接异常断开");
},
);
//心跳
_heartbeatTimer?.cancel();
_heartbeatTimer = Timer.periodic(Duration(seconds: 15), (_) {
logger.i("发送心跳");
});
} catch (e) {
_reconnect();
}
}
///发送指令
void send() {
_socket!.add("");
}
///连接错误事件
void _reconnect() {
logger.e("连接错误");
Future.delayed(Duration(seconds: 3), connect);
}
void dispose() {
_heartbeatTimer?.cancel();
_socket?.close();
_msgController.close();
}
}

View File

@@ -34,7 +34,7 @@ class Button extends StatelessWidget {
};
return Opacity(
opacity: disabled ? 0.5 : 1,
opacity: disabled || loading ? 0.5 : 1,
child: Container(
width: width,
decoration: bgDecoration.copyWith(borderRadius: radius),
@@ -47,14 +47,27 @@ class Button extends StatelessWidget {
highlightColor: Colors.white.withValues(alpha: 0.2),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7),
child: Center(
child: Text(
text,
style: TextStyle(
color: type != ThemeType.info ? Colors.white : Colors.black,
child: Row(
spacing: 10,
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (loading)
const SizedBox(
width: 15,
height: 15,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation(Colors.white),
),
),
Text(
text,
style: TextStyle(
color: type != ThemeType.info ? Colors.white : Colors.black,
),
textAlign: TextAlign.center,
),
textAlign: TextAlign.center,
),
],
),
),
),

View File

@@ -0,0 +1,53 @@
import 'package:flutter/material.dart';
enum EmptyType {
data,
}
class Empty extends StatelessWidget {
final EmptyType type;
final String? text;
final Widget? child;
const Empty({
super.key,
this.type = EmptyType.data,
this.text,
this.child,
});
@override
Widget build(BuildContext context) {
var emptyImg = switch (type) {
EmptyType.data => Image.asset('assets/image/empty_data.png'),
};
var emptyText = switch (type) {
EmptyType.data => '暂无数据',
};
return Container(
padding: EdgeInsets.all(0),
child: Column(
children: [
FractionallySizedBox(
widthFactor: 0.5,
child: Container(
margin: EdgeInsets.only(bottom: 15),
child: emptyImg,
),
),
Text(
text ?? emptyText,
style: Theme.of(context).textTheme.labelLarge,
textAlign: TextAlign.center,
),
child != null
? Container(
margin: EdgeInsets.only(top: 15),
child: child,
)
: SizedBox(),
],
),
);
}
}

View File

@@ -1,6 +1,14 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
agora_rtc_engine:
dependency: "direct main"
description:
name: agora_rtc_engine
sha256: "6559294d18ce4445420e19dbdba10fb58cac955cd8f22dbceae26716e194d70e"
url: "https://pub.flutter-io.cn"
source: hosted
version: "6.5.3"
async:
dependency: transitive
description:
@@ -237,6 +245,30 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.20.2"
iris_method_channel:
dependency: transitive
description:
name: iris_method_channel
sha256: bfb5cfc6c6eae42da8cd1b35977a72d8b8881848a5dfc3d672e4760a907d11a0
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.2.4"
js:
dependency: transitive
description:
name: js
sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.7.2"
json_annotation:
dependency: transitive
description:
name: json_annotation
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.9.0"
leak_tracker:
dependency: transitive
description:
@@ -549,6 +581,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.1"
skeletonizer:
dependency: "direct main"
description:
name: skeletonizer
sha256: eebc03dc86b298e2d7f61e0ebce5713e9dbbc3e786f825909b4591756f196eb6
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.0+1"
sky_engine:
dependency: transitive
description: flutter
@@ -708,4 +748,4 @@ packages:
version: "1.1.0"
sdks:
dart: ">=3.8.1 <4.0.0"
flutter: ">=3.29.0"
flutter: ">=3.31.0-0.0.pre"

View File

@@ -15,7 +15,7 @@ dependencies:
intl: any
cupertino_icons: ^1.0.8
go_router: ^16.2.4
permission_handler: ^12.0.0+1
permission_handler: ^12.0.1
provider: ^6.1.5
shared_preferences: ^2.5.3
dio: ^5.8.0+1
@@ -26,6 +26,8 @@ dependencies:
flutter_easyloading: ^3.0.5
cached_network_image: ^3.4.1
flutter_cached_pdfview: ^0.4.3
skeletonizer: ^2.1.0+1
agora_rtc_engine: ^6.5.3
dev_dependencies:
flutter_test:
@@ -34,4 +36,5 @@ dev_dependencies:
flutter:
uses-material-design: true
generate: true
assets:
- assets/image/