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,16 +1,34 @@
<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
@@ -18,8 +36,7 @@
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
android:resource="@style/NormalTheme" />
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />

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;
}
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 {
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,28 +1,44 @@
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),
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,10 +36,28 @@ class Header extends StatelessWidget implements PreferredSizeWidget {
),
Row(
children: [
IconButton(
onPressed: () {},
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,19 +1,138 @@
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({
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: [
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: [
_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: 30),
height: 45,
child: Button(
text: vm.canEnterRoom ? "开始自习室" : "未到开始时间",
type: ThemeType.success,
// disabled: !vm.canEnterRoom,
onPressed: _goToRoom,
),
),
],
),
),
child: SizedBox(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Empty(text: "未分配自习室"),
],
),
),
),
);
},
);
}
Widget _item({
required String title,
required String value,
required IconData icon,
@@ -53,76 +172,4 @@ class TodayCard extends StatelessWidget {
),
);
}
return GCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
spacing: 10,
children: [
Text("高三数学冲刺班"),
Tag(text: "待开始"),
],
),
Container(
margin: EdgeInsets.only(top: 5),
child: Text(
"和学生们一起专注学习、共同进步",
style: Theme.of(context).textTheme.labelMedium,
),
),
],
),
),
Icon(RemixIcons.arrow_right_s_line, size: 30),
],
),
Container(
margin: EdgeInsets.only(top: 30),
child: Row(
spacing: 15,
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),
),
],
),
),
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(
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,
),
],
),
),
),

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/