1
This commit is contained in:
@@ -6,7 +6,7 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "com.zkwl.xueguang.xueguang_flutter_app"
|
namespace = "com.zkwl.xueguang"
|
||||||
compileSdk = flutter.compileSdkVersion
|
compileSdk = flutter.compileSdkVersion
|
||||||
ndkVersion = "27.0.12077973"
|
ndkVersion = "27.0.12077973"
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ android {
|
|||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
// 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.
|
// You can update the following values to match your application needs.
|
||||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||||
minSdk = flutter.minSdkVersion
|
minSdk = flutter.minSdkVersion
|
||||||
|
|||||||
@@ -1,28 +1,45 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<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
|
<application
|
||||||
android:label="xueguang_flutter_app"
|
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher">
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="学光自习室">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
android:hardwareAccelerated="true"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop"
|
||||||
android:taskAffinity=""
|
android:taskAffinity=""
|
||||||
android:theme="@style/LaunchTheme"
|
android:theme="@style/LaunchTheme"
|
||||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
|
||||||
android:hardwareAccelerated="true"
|
|
||||||
android:windowSoftInputMode="adjustResize">
|
android:windowSoftInputMode="adjustResize">
|
||||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||||
the Android process has started. This theme is visible to the user
|
the Android process has started. This theme is visible to the user
|
||||||
while the Flutter UI initializes. After that, this theme continues
|
while the Flutter UI initializes. After that, this theme continues
|
||||||
to determine the Window background behind the Flutter UI. -->
|
to determine the Window background behind the Flutter UI. -->
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="io.flutter.embedding.android.NormalTheme"
|
android:name="io.flutter.embedding.android.NormalTheme"
|
||||||
android:resource="@style/NormalTheme"
|
android:resource="@style/NormalTheme" />
|
||||||
/>
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<!-- Don't delete the meta-data below.
|
<!-- 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. -->
|
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
||||||
<queries>
|
<queries>
|
||||||
<intent>
|
<intent>
|
||||||
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
<action android:name="android.intent.action.PROCESS_TEXT" />
|
||||||
<data android:mimeType="text/plain"/>
|
<data android:mimeType="text/plain" />
|
||||||
</intent>
|
</intent>
|
||||||
</queries>
|
</queries>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.zkwl.xueguang.xueguang_flutter_app
|
package com.zkwl.xueguang
|
||||||
|
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
|
||||||
|
|||||||
BIN
assets/image/empty_data.png
Normal file
BIN
assets/image/empty_data.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 327 KiB |
3
devtools_options.yaml
Normal file
3
devtools_options.yaml
Normal 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:
|
||||||
@@ -8,9 +8,17 @@ class Config {
|
|||||||
///获取接口地址
|
///获取接口地址
|
||||||
static String baseUrl() {
|
static String baseUrl() {
|
||||||
if (getEnv() == 'dev') {
|
if (getEnv() == 'dev') {
|
||||||
return 'https://mindapp.test.tuzuu.com/api';
|
return 'https://xueguang.test.tuzuu.com/api';
|
||||||
} else {
|
} 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";
|
||||||
}
|
}
|
||||||
|
|||||||
50
lib/data/local/storage.dart
Normal file
50
lib/data/local/storage.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,22 @@
|
|||||||
|
import 'package:app/providers/user_store.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
import 'package:app/router/routes.dart';
|
import 'package:app/router/routes.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import 'config/theme/theme.dart';
|
import 'config/theme/theme.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
runApp(const MyApp());
|
runApp(
|
||||||
|
MultiProvider(
|
||||||
|
providers: [
|
||||||
|
ChangeNotifierProvider(create: (_) => UserStore()),
|
||||||
|
],
|
||||||
|
child: const MyApp(),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class MyApp extends StatelessWidget {
|
class MyApp extends StatelessWidget {
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import 'dart:async';
|
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/router/route_paths.dart';
|
||||||
import 'package:app/widgets/base/button/index.dart';
|
import 'package:app/widgets/base/button/index.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
import 'package:remixicon/remixicon.dart';
|
import 'package:remixicon/remixicon.dart';
|
||||||
|
|
||||||
import 'widgets/login_agree.dart';
|
|
||||||
import 'widgets/login_input.dart';
|
import 'widgets/login_input.dart';
|
||||||
|
|
||||||
class LoginPage extends StatefulWidget {
|
class LoginPage extends StatefulWidget {
|
||||||
@@ -23,8 +25,11 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
bool _agree = false;
|
bool _agree = false;
|
||||||
|
|
||||||
///输入框
|
///输入框
|
||||||
final TextEditingController _telController = TextEditingController();
|
final TextEditingController _telController = TextEditingController(text: "13343214321");
|
||||||
final TextEditingController _codeController = TextEditingController();
|
final TextEditingController _codeController = TextEditingController(text: "1111");
|
||||||
|
|
||||||
|
///登录中
|
||||||
|
bool _loading = false;
|
||||||
|
|
||||||
///验证码倒计时
|
///验证码倒计时
|
||||||
var _countDown = 0;
|
var _countDown = 0;
|
||||||
@@ -42,6 +47,7 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
EasyLoading.showToast("请填写正确的手机号");
|
EasyLoading.showToast("请填写正确的手机号");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
sendCodeApi(_telController.text);
|
||||||
setState(() {
|
setState(() {
|
||||||
_countDown = 60;
|
_countDown = 60;
|
||||||
});
|
});
|
||||||
@@ -63,7 +69,29 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
EasyLoading.showToast("请填写完整手机号或验证码");
|
EasyLoading.showToast("请填写完整手机号或验证码");
|
||||||
return;
|
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
|
@override
|
||||||
@@ -130,21 +158,25 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
Container(
|
Container(
|
||||||
margin: EdgeInsets.only(top: 40),
|
margin: EdgeInsets.only(top: 40),
|
||||||
height: 50,
|
height: 50,
|
||||||
child: Button(text: "登 录", onPressed: _handSubmit),
|
child: Button(
|
||||||
),
|
text: "登 录",
|
||||||
Container(
|
loading: _loading,
|
||||||
width: double.infinity,
|
onPressed: _handSubmit,
|
||||||
margin: EdgeInsets.only(top: 20),
|
|
||||||
alignment: Alignment.center,
|
|
||||||
child: LoginAgree(
|
|
||||||
value: _agree,
|
|
||||||
onChanged: (value) {
|
|
||||||
setState(() {
|
|
||||||
_agree = value!;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
// Container(
|
||||||
|
// width: double.infinity,
|
||||||
|
// margin: EdgeInsets.only(top: 20),
|
||||||
|
// alignment: Alignment.center,
|
||||||
|
// child: LoginAgree(
|
||||||
|
// value: _agree,
|
||||||
|
// onChanged: (value) {
|
||||||
|
// setState(() {
|
||||||
|
// _agree = value!;
|
||||||
|
// });
|
||||||
|
// },
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
import 'package:app/providers/user_store.dart';
|
||||||
import 'package:app/router/route_paths.dart';
|
import 'package:app/router/route_paths.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class SplashPage extends StatefulWidget {
|
class SplashPage extends StatefulWidget {
|
||||||
const SplashPage({super.key});
|
const SplashPage({super.key});
|
||||||
@@ -19,7 +21,24 @@ class _SplashPageState extends State<SplashPage> {
|
|||||||
///权限效验初始化
|
///权限效验初始化
|
||||||
void initPermission() async {
|
void initPermission() async {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import 'package:app/config/theme/base/app_theme_ext.dart';
|
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 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'today/s_today_card.dart';
|
import 'today/s_today_card.dart';
|
||||||
@@ -12,6 +14,8 @@ class SHomePage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _SHomePageState extends State<SHomePage> {
|
class _SHomePageState extends State<SHomePage> {
|
||||||
|
|
||||||
|
|
||||||
///刷新状态
|
///刷新状态
|
||||||
Future<void> _refresh() async {
|
Future<void> _refresh() async {
|
||||||
await Future.delayed(Duration(seconds: 1));
|
await Future.delayed(Duration(seconds: 1));
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ class STodayCard extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _STodayCardState extends State<STodayCard> {
|
class _STodayCardState extends State<STodayCard> {
|
||||||
|
|
||||||
///进入自习室
|
///进入自习室
|
||||||
void _handleEnterRoom() {
|
void _handleEnterRoom() {
|
||||||
context.push(RoutePaths.sRoom);
|
context.push(RoutePaths.sRoom);
|
||||||
|
|||||||
@@ -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:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
import 'package:remixicon/remixicon.dart';
|
import 'package:remixicon/remixicon.dart';
|
||||||
|
|
||||||
class UserHeader extends StatelessWidget implements PreferredSizeWidget {
|
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),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,43 @@
|
|||||||
import 'package:app/config/theme/base/app_theme_ext.dart';
|
import 'package:app/config/theme/base/app_theme_ext.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import 'viewmodel/home_view_model.dart';
|
||||||
import 'widgets/header.dart';
|
import 'widgets/header.dart';
|
||||||
import 'widgets/today_card.dart';
|
import 'widgets/today_card.dart';
|
||||||
|
|
||||||
class THomePage extends StatefulWidget {
|
class THomePage extends StatelessWidget {
|
||||||
const THomePage({super.key});
|
const THomePage({super.key});
|
||||||
|
|
||||||
@override
|
@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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final vm = context.read<HomeViewModel>();
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
|
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
appBar: Header(),
|
appBar: Header(),
|
||||||
body: ListView(
|
body: RefreshIndicator(
|
||||||
padding: EdgeInsets.symmetric(vertical: 20, horizontal: context.pagePadding),
|
onRefresh: vm.loadData,
|
||||||
children: [
|
child: ListView(
|
||||||
TodayCard(),
|
padding: EdgeInsets.symmetric(
|
||||||
],
|
vertical: 20,
|
||||||
|
horizontal: context.pagePadding,
|
||||||
|
),
|
||||||
|
children: [
|
||||||
|
TodayCard(),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
56
lib/pages/teacher/home/viewmodel/home_view_model.dart
Normal file
56
lib/pages/teacher/home/viewmodel/home_view_model.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
import 'package:app/config/theme/base/app_theme_ext.dart';
|
import 'package:app/config/theme/base/app_theme_ext.dart';
|
||||||
|
import 'package:app/providers/user_store.dart';
|
||||||
|
import 'package:app/router/route_paths.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
import 'package:remixicon/remixicon.dart';
|
import 'package:remixicon/remixicon.dart';
|
||||||
|
|
||||||
class Header extends StatelessWidget implements PreferredSizeWidget {
|
class Header extends StatelessWidget implements PreferredSizeWidget {
|
||||||
@@ -18,6 +22,7 @@ class Header extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
children: [
|
children: [
|
||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
"学光自习室",
|
"学光自习室",
|
||||||
@@ -31,9 +36,27 @@ class Header extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
PopupMenuButton(
|
||||||
onPressed: () {},
|
color: Colors.white,
|
||||||
icon: Icon(RemixIcons.user_line),
|
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),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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/router/route_paths.dart';
|
||||||
|
import 'package:app/utils/permission.dart';
|
||||||
import 'package:app/widgets/base/button/index.dart';
|
import 'package:app/widgets/base/button/index.dart';
|
||||||
import 'package:app/widgets/base/card/g_card.dart';
|
import 'package:app/widgets/base/card/g_card.dart';
|
||||||
import 'package:app/widgets/base/config/config.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/material.dart';
|
||||||
|
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
import 'package:remixicon/remixicon.dart';
|
import 'package:remixicon/remixicon.dart';
|
||||||
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
|
|
||||||
class TodayCard extends StatelessWidget {
|
class TodayCard extends StatefulWidget {
|
||||||
const TodayCard({super.key});
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
/// item
|
return Consumer<HomeViewModel>(
|
||||||
Widget item({
|
builder: (context, vm, _) {
|
||||||
required String title,
|
return GCard(
|
||||||
required String value,
|
child: Visibility(
|
||||||
required IconData icon,
|
visible: !vm.loading && vm.roomInfo == null,
|
||||||
required Color color,
|
replacement: Skeletonizer(
|
||||||
}) {
|
enabled: vm.loading,
|
||||||
return Expanded(
|
child: Column(
|
||||||
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,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(title, style: Theme.of(context).textTheme.labelLarge),
|
Column(
|
||||||
Text(value),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
],
|
children: [
|
||||||
),
|
Row(
|
||||||
],
|
spacing: 10,
|
||||||
),
|
children: [
|
||||||
),
|
Skeleton.replace(
|
||||||
);
|
replacement: Bone.text(),
|
||||||
}
|
child: Text(vm.roomInfo?.roomName ?? ""),
|
||||||
|
),
|
||||||
return GCard(
|
],
|
||||||
child: Column(
|
),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
Container(
|
||||||
children: [
|
margin: EdgeInsets.only(top: 5),
|
||||||
Row(
|
child: Text(
|
||||||
children: [
|
"和学生们一起专注学习、共同进步",
|
||||||
Expanded(
|
style: Theme.of(context).textTheme.labelMedium,
|
||||||
child: Column(
|
),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
),
|
||||||
children: [
|
],
|
||||||
Row(
|
),
|
||||||
spacing: 10,
|
Container(
|
||||||
|
margin: EdgeInsets.only(top: 30),
|
||||||
|
child: Row(
|
||||||
|
spacing: 15,
|
||||||
children: [
|
children: [
|
||||||
Text("高三数学冲刺班"),
|
_item(
|
||||||
Tag(text: "待开始"),
|
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),
|
Container(
|
||||||
child: Text(
|
margin: EdgeInsets.only(top: 30),
|
||||||
"和学生们一起专注学习、共同进步",
|
height: 45,
|
||||||
style: Theme.of(context).textTheme.labelMedium,
|
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: [
|
children: [
|
||||||
item(
|
Text(title, style: Theme.of(context).textTheme.labelLarge),
|
||||||
title: "开始时间",
|
Text(value),
|
||||||
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),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
Container(
|
),
|
||||||
margin: EdgeInsets.only(top: 30),
|
|
||||||
height: 45,
|
|
||||||
child: Button(
|
|
||||||
text: "开始自习室",
|
|
||||||
type: ThemeType.success,
|
|
||||||
onPressed: () {
|
|
||||||
context.push(RoutePaths.tRoom);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'controls/top_bar.dart';
|
import 'controls/top_bar.dart';
|
||||||
import 'view/student_item.dart';
|
import 'widgets/status_view.dart';
|
||||||
import 'view/waiting_start.dart';
|
|
||||||
import 'viewmodel/students_view_model.dart';
|
import 'viewmodel/students_view_model.dart';
|
||||||
|
|
||||||
class TRoomPage extends StatefulWidget {
|
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
|
@override
|
||||||
State<TRoomPage> createState() => _TRoomPageState();
|
State<TRoomPage> createState() => _TRoomPageState();
|
||||||
@@ -17,37 +23,15 @@ class _TRoomPageState extends State<TRoomPage> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ChangeNotifierProvider<StudentsViewModel>(
|
return ChangeNotifierProvider<StudentsViewModel>(
|
||||||
create: (BuildContext context) {
|
create: (BuildContext context) {
|
||||||
return StudentsViewModel();
|
return StudentsViewModel(
|
||||||
|
roomId: widget.roomId,
|
||||||
|
start: widget.startTime,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
backgroundColor: Color(0xff2c3032),
|
backgroundColor: Color(0xff2c3032),
|
||||||
appBar: TopBar(),
|
appBar: TopBar(),
|
||||||
body: true
|
body: StatusView(),
|
||||||
? 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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +1,78 @@
|
|||||||
import 'package:app/data/models/student.dart';
|
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';
|
import 'package:flutter/cupertino.dart';
|
||||||
|
|
||||||
class StudentsViewModel extends ChangeNotifier {
|
class StudentsViewModel extends ChangeNotifier {
|
||||||
///学生摄像头列表
|
///学生摄像头列表
|
||||||
List<Student> _students = [];
|
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;
|
List<Student> get students => _students;
|
||||||
|
|
||||||
|
///是否能开始自习室
|
||||||
|
bool get canEnterRoom {
|
||||||
|
final now = DateTime.now();
|
||||||
|
|
||||||
|
// 如果到了开始时间,则可以进入房间
|
||||||
|
if (now.isAfter(startTime)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
///websocket管理
|
///websocket管理
|
||||||
late RoomWebSocket _ws;
|
late RoomWebSocket _ws;
|
||||||
|
|
||||||
StudentsViewModel() {
|
|
||||||
_startRoom();
|
|
||||||
}
|
|
||||||
|
|
||||||
///开始链接房间
|
///开始链接房间
|
||||||
void _startRoom() {
|
void _startRoom() async {
|
||||||
_ws = RoomWebSocket();
|
_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) {
|
_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();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
///发送命令
|
///自习室的开关
|
||||||
void _handleMessage() {
|
/// - [isOpen]: 是否开启
|
||||||
print("监听webscoket传来的事件");
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
35
lib/pages/teacher/room/widgets/content_view.dart
Normal file
35
lib/pages/teacher/room/widgets/content_view.dart
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
114
lib/pages/teacher/room/widgets/status_view.dart
Normal file
114
lib/pages/teacher/room/widgets/status_view.dart
Normal 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, //房间开始中
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
26
lib/request/api/room_api.dart
Normal file
26
lib/request/api/room_api.dart
Normal 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);
|
||||||
|
}
|
||||||
29
lib/request/api/user_api.dart
Normal file
29
lib/request/api/user_api.dart
Normal 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");
|
||||||
|
}
|
||||||
24
lib/request/dto/room/room_file_dto.dart
Normal file
24
lib/request/dto/room/room_file_dto.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
39
lib/request/dto/room/room_info_dto.dart
Normal file
39
lib/request/dto/room/room_info_dto.dart
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
39
lib/request/dto/room/room_type_dto.dart
Normal file
39
lib/request/dto/room/room_type_dto.dart
Normal 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"] ?? "",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
67
lib/request/dto/room/room_user_dto.dart
Normal file
67
lib/request/dto/room/room_user_dto.dart
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
27
lib/request/dto/room/rtc_token_dto.dart
Normal file
27
lib/request/dto/room/rtc_token_dto.dart
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
14
lib/request/dto/user/login_dto.dart
Normal file
14
lib/request/dto/user/login_dto.dart
Normal 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"] ?? "";
|
||||||
|
}
|
||||||
60
lib/request/dto/user/user_info_dto.dart
Normal file
60
lib/request/dto/user/user_info_dto.dart
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,13 +1,16 @@
|
|||||||
|
import 'package:app/providers/user_store.dart';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||||
|
|
||||||
import '../dto/base_dto.dart';
|
import '../dto/base_dto.dart';
|
||||||
|
|
||||||
|
|
||||||
///请求拦截器
|
///请求拦截器
|
||||||
void onRequest(
|
void onRequest(
|
||||||
RequestOptions options,
|
RequestOptions options,
|
||||||
RequestInterceptorHandler handler,
|
RequestInterceptorHandler handler,
|
||||||
) {
|
) async {
|
||||||
|
String token = await UserStore.getToken();
|
||||||
|
options.headers['Authorization'] = 'Bearer $token';
|
||||||
return handler.next(options);
|
return handler.next(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18,6 +21,7 @@ void onResponse(
|
|||||||
) {
|
) {
|
||||||
var apiResponse = ApiDto.fromJson(response.data);
|
var apiResponse = ApiDto.fromJson(response.data);
|
||||||
if (apiResponse.code == 1) {
|
if (apiResponse.code == 1) {
|
||||||
|
response.data = apiResponse.data;
|
||||||
handler.next(response);
|
handler.next(response);
|
||||||
} else {
|
} else {
|
||||||
handler.reject(
|
handler.reject(
|
||||||
@@ -35,17 +39,25 @@ void onError(
|
|||||||
DioException e,
|
DioException e,
|
||||||
ErrorInterceptorHandler handler,
|
ErrorInterceptorHandler handler,
|
||||||
) {
|
) {
|
||||||
|
var title = "";
|
||||||
if (e.type == DioExceptionType.connectionTimeout) {
|
if (e.type == DioExceptionType.connectionTimeout) {
|
||||||
print("请求超时");
|
title = "请求超时";
|
||||||
} else if (e.type == DioExceptionType.badResponse) {
|
} else if (e.type == DioExceptionType.badResponse) {
|
||||||
if (e.response?.statusCode == 404) {
|
if (e.response?.statusCode == 404) {
|
||||||
print("接口404不存在");
|
title = "接口404不存在";
|
||||||
} else {
|
} else {
|
||||||
print("500");
|
title = "500";
|
||||||
}
|
}
|
||||||
} else if (e.type == DioExceptionType.connectionError) {
|
} else if (e.type == DioExceptionType.connectionError) {
|
||||||
print("网络连接失败");
|
title = "网络连接失败";
|
||||||
} else {
|
} else {
|
||||||
print("接口请求异常报错");
|
title = "异常其他错误";
|
||||||
}
|
}
|
||||||
|
showError(title);
|
||||||
|
handler.next(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
///显示错误信息
|
||||||
|
void showError(String message) {
|
||||||
|
EasyLoading.showError(message);
|
||||||
}
|
}
|
||||||
|
|||||||
103
lib/request/websocket/room_protocol.dart
Normal file
103
lib/request/websocket/room_protocol.dart
Normal 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'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
122
lib/request/websocket/room_websocket.dart
Normal file
122
lib/request/websocket/room_websocket.dart
Normal 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);
|
||||||
|
}
|
||||||
@@ -14,7 +14,13 @@ List<RouterConfig> teacherRoutes = [
|
|||||||
RouterConfig(
|
RouterConfig(
|
||||||
path: RoutePaths.tRoom,
|
path: RoutePaths.tRoom,
|
||||||
child: (state) {
|
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!,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -23,6 +23,6 @@ List<RouteBase> routes = routeConfigs.map((item) {
|
|||||||
|
|
||||||
//变量命名
|
//变量命名
|
||||||
GoRouter goRouter = GoRouter(
|
GoRouter goRouter = GoRouter(
|
||||||
initialLocation: RoutePaths.tHome,
|
initialLocation: RoutePaths.splash,
|
||||||
routes: routes,
|
routes: routes,
|
||||||
);
|
);
|
||||||
|
|||||||
65
lib/utils/permission.dart
Normal file
65
lib/utils/permission.dart
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -46,3 +46,16 @@ String formatSeconds(int seconds) {
|
|||||||
return '${twoDigits(m)}:${twoDigits(s)}';
|
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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -34,7 +34,7 @@ class Button extends StatelessWidget {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return Opacity(
|
return Opacity(
|
||||||
opacity: disabled ? 0.5 : 1,
|
opacity: disabled || loading ? 0.5 : 1,
|
||||||
child: Container(
|
child: Container(
|
||||||
width: width,
|
width: width,
|
||||||
decoration: bgDecoration.copyWith(borderRadius: radius),
|
decoration: bgDecoration.copyWith(borderRadius: radius),
|
||||||
@@ -47,14 +47,27 @@ class Button extends StatelessWidget {
|
|||||||
highlightColor: Colors.white.withValues(alpha: 0.2),
|
highlightColor: Colors.white.withValues(alpha: 0.2),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7),
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7),
|
||||||
child: Center(
|
child: Row(
|
||||||
child: Text(
|
spacing: 10,
|
||||||
text,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
style: TextStyle(
|
children: [
|
||||||
color: type != ThemeType.info ? Colors.white : Colors.black,
|
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,
|
],
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
53
lib/widgets/base/empty/index.dart
Normal file
53
lib/widgets/base/empty/index.dart
Normal 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(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
42
pubspec.lock
42
pubspec.lock
@@ -1,6 +1,14 @@
|
|||||||
# Generated by pub
|
# Generated by pub
|
||||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||||
packages:
|
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:
|
async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -237,6 +245,30 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.20.2"
|
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:
|
leak_tracker:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -549,6 +581,14 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.1"
|
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:
|
sky_engine:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -708,4 +748,4 @@ packages:
|
|||||||
version: "1.1.0"
|
version: "1.1.0"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.8.1 <4.0.0"
|
dart: ">=3.8.1 <4.0.0"
|
||||||
flutter: ">=3.29.0"
|
flutter: ">=3.31.0-0.0.pre"
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ dependencies:
|
|||||||
intl: any
|
intl: any
|
||||||
cupertino_icons: ^1.0.8
|
cupertino_icons: ^1.0.8
|
||||||
go_router: ^16.2.4
|
go_router: ^16.2.4
|
||||||
permission_handler: ^12.0.0+1
|
permission_handler: ^12.0.1
|
||||||
provider: ^6.1.5
|
provider: ^6.1.5
|
||||||
shared_preferences: ^2.5.3
|
shared_preferences: ^2.5.3
|
||||||
dio: ^5.8.0+1
|
dio: ^5.8.0+1
|
||||||
@@ -26,6 +26,8 @@ dependencies:
|
|||||||
flutter_easyloading: ^3.0.5
|
flutter_easyloading: ^3.0.5
|
||||||
cached_network_image: ^3.4.1
|
cached_network_image: ^3.4.1
|
||||||
flutter_cached_pdfview: ^0.4.3
|
flutter_cached_pdfview: ^0.4.3
|
||||||
|
skeletonizer: ^2.1.0+1
|
||||||
|
agora_rtc_engine: ^6.5.3
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
@@ -34,4 +36,5 @@ dev_dependencies:
|
|||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
generate: true
|
assets:
|
||||||
|
- assets/image/
|
||||||
Reference in New Issue
Block a user