自习室优化ok

This commit is contained in:
zhutao
2025-11-28 13:31:23 +08:00
parent 4ecb0c35d6
commit 57305c5804
57 changed files with 2500 additions and 597 deletions

View File

@@ -0,0 +1,93 @@
import 'dart:async';
import 'package:app/data/models/meeting_room_dto.dart';
import 'package:app/utils/time.dart';
import 'package:flutter/cupertino.dart';
class CountDownVM extends ChangeNotifier {
MeetingRoomDto? roomInfo;
///会议开始倒计时秒数
Timer? _startTime;
int _startCountDown = 0;
///会议结束倒计时秒数
Timer? _endTime;
int _endCountDown = -1;
///会议进行中的秒数
int studyTime = 0;
int get startCountDown => _startCountDown;
int get endCountDown => _endCountDown;
///是否能开始自习室
bool get canEnterRoom {
final now = DateTime.now();
if (now.isAfter(parseTime(roomInfo!.startTime))) {
return true;
}
return false;
}
//绑定
void bind(MeetingRoomDto info) {
if (roomInfo == info) return;
roomInfo = info;
_startEndCountdown();
//如果会议室结束,停止计时器
if (roomInfo?.roomStatus == 2) {
_endTime?.cancel();
}
}
///启动距离会议结束还有多少秒
void _startEndCountdown() {
if (roomInfo!.actualStartTime.isEmpty || roomInfo!.roomStatus != 1) return;
_endTime?.cancel();
DateTime endTime = parseTime(roomInfo!.endTime);
DateTime startTime = DateTime.parse(roomInfo!.actualStartTime);
_endCountDown = endTime.difference(startTime).inSeconds;
_endTime = Timer.periodic(Duration(seconds: 1), (timer) {
_endCountDown--;
studyTime++;
if (_endCountDown <= 0) {
_endTime?.cancel();
}
notifyListeners();
});
}
///启动距离会议开始还有多少秒
void startStartCountdown() {
if (roomInfo?.roomStatus != 0) return;
_startTime?.cancel();
final now = DateTime.now();
final startTime = parseTime(roomInfo!.startTime);
_startCountDown = startTime.difference(now).inSeconds;
if (_startCountDown <= 0) {
return;
}
_startTime = Timer.periodic(Duration(seconds: 1), (timer) {
_startCountDown--;
if (_startCountDown <= 0) {
_startTime?.cancel();
_startTime = null;
}
notifyListeners();
});
}
@override
void dispose() {
_startTime?.cancel();
_endTime?.cancel();
super.dispose();
}
}

View File

@@ -1,6 +1,14 @@
import 'dart:io';
import 'package:app/config/theme/base/app_theme_ext.dart';
import 'package:app/utils/transfer/upload.dart';
import 'package:app/widgets/base/actionSheet/action_sheet.dart';
import 'package:app/widgets/base/actionSheet/type.dart';
import 'package:app/widgets/base/button/index.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:image_picker/image_picker.dart';
import '../common/preview/file_previewer.dart';
@@ -8,6 +16,9 @@ import '../common/preview/file_previewer.dart';
void showFileDialog(
BuildContext context, {
bool isUpload = true,
String? name,
List<String> files = const [],
ValueChanged<List<String>>? onConfirm,
}) {
showGeneralDialog(
context: context,
@@ -16,7 +27,10 @@ void showFileDialog(
barrierLabel: "RightSheet",
pageBuilder: (context, animation, secondaryAnimation) {
return FileDrawer(
name: name,
isUpload: isUpload,
files: files,
onConfirm: onConfirm,
);
},
transitionBuilder: (context, animation, secondaryAnimation, child) {
@@ -31,15 +45,99 @@ void showFileDialog(
///文件弹窗
class FileDrawer extends StatefulWidget {
final String? name;
final List<String> files;
final bool isUpload;
final ValueChanged<List<String>>? onConfirm;
const FileDrawer({super.key, this.isUpload = true});
const FileDrawer({
super.key,
this.name,
this.isUpload = true,
this.files = const [],
this.onConfirm,
});
@override
State<FileDrawer> createState() => _FileDrawerState();
}
class _FileDrawerState extends State<FileDrawer> {
///文件列表
List<String> _fileList = [];
@override
void initState() {
super.initState();
_fileList = List<String>.from(widget.files);
}
///打开选择面板
void _handOpenActionSheet() {
showActionSheet(
context,
showCancel: true,
actions: [
ActionSheetItem(title: "拍照", value: 1),
ActionSheetItem(title: "选择图片", value: 2),
ActionSheetItem(title: "选择PDF", value: 3),
],
onConfirm: (res) async {
List<File> filesToUpload = [];
try {
if (res.value == 1) {
final ImagePicker picker = ImagePicker();
final XFile? photo = await picker.pickImage(source: ImageSource.camera);
if (photo == null) return; // 用户取消
filesToUpload.add(File(photo.path));
} else if (res.value == 2) {
//选择图片
final result = await FilePicker.platform.pickFiles(
allowMultiple: true,
type: FileType.image,
);
if (result == null) return;
filesToUpload.addAll(result.paths.whereType<String>().map((e) => File(e)));
} else if (res.value == 3) {
//选择pdf
final result = await FilePicker.platform.pickFiles(
allowMultiple: true,
type: FileType.custom,
allowedExtensions: ['pdf'],
);
if (result == null) return;
filesToUpload.addAll(result.paths.whereType<String>().map((e) => File(e)));
}
if (filesToUpload.isEmpty) return;
// 4) 上传文件
EasyLoading.show(status: "文件上传中");
final uploadTasks = filesToUpload.map((file) {
return QinUpload.upload(
file: file,
path: "room/classroom",
);
});
final List<String> uploadedPaths = (await Future.wait(
uploadTasks,
)).whereType<String>().toList();
EasyLoading.dismiss();
// 更新 UI
setState(() => _fileList.addAll(uploadedPaths));
// 回调
widget.onConfirm?.call(uploadedPaths);
} catch (e) {
print(e);
EasyLoading.showToast("文件上传失败");
}
},
);
}
@override
Widget build(BuildContext context) {
return Align(
@@ -52,39 +150,45 @@ class _FileDrawerState extends State<FileDrawer> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'上传文件列表',
"${widget.name ?? ""}上传文件列表",
style: Theme.of(context).textTheme.titleSmall,
),
Expanded(
child: ListView.separated(
padding: EdgeInsets.symmetric(vertical: 15),
itemBuilder: (_, index) {
return InkWell(
onTap: () {
showFilePreviewer(
context,
url: "https://doaf.asia/api/assets/1/图/65252305_p0.jpg",
);
},
child: Container(
padding: EdgeInsets.symmetric(vertical: 10, horizontal: 10),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(5),
child: Visibility(
visible: _fileList.isNotEmpty,
replacement: Align(
child: Text("未上传文件"),
),
child: ListView.separated(
padding: EdgeInsets.symmetric(vertical: 15),
itemBuilder: (_, index) {
String item = _fileList[index];
String suffix = item.split(".").last;
return InkWell(
key: Key(item),
onTap: () {
showFilePreviewer(context, url: item);
},
child: Container(
padding: EdgeInsets.symmetric(vertical: 10, horizontal: 10),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(5),
),
child: Text("文件${index + 1}.$suffix", style: TextStyle(fontSize: 14)),
),
child: Text("文件1.png", style: TextStyle(fontSize: 14)),
),
);
},
separatorBuilder: (_, __) => SizedBox(height: 15),
itemCount: 15,
);
},
separatorBuilder: (_, __) => SizedBox(height: 15),
itemCount: _fileList.length,
),
),
),
Visibility(
visible: widget.isUpload,
child: Button(
text: "上传",
onPressed: () {},
onPressed: _handOpenActionSheet,
),
),
],
@@ -93,3 +197,12 @@ class _FileDrawerState extends State<FileDrawer> {
);
}
}
///数据类
class UploadFileItem {
String url;
String name;
bool loading;
UploadFileItem({required this.url, this.loading = false, required this.name});
}

View File

@@ -0,0 +1,26 @@
import 'package:flutter/material.dart';
class HandRaiseButton extends StatelessWidget {
final void Function() onTap;
const HandRaiseButton({super.key, required this.onTap});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
child: Container(
height: 60,
width: 60,
decoration: BoxDecoration(
color: Colors.black12,
shape: BoxShape.circle,
),
child: Icon(
Icons.back_hand_rounded,
color: Color(0xFFFDC400),
size: 24,
),
),
);
}
}

View File

@@ -1,46 +1,52 @@
import 'package:app/config/config.dart';
import 'package:app/request/dto/room/room_user_dto.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:agora_rtc_engine/agora_rtc_engine.dart';
import '../../request/dto/room/rtc_token_dto.dart';
/// 视频画面显示状态
enum VideoState {
/// 正常显示视频
normal,
/// 摄像头关闭
closed,
/// 掉线 / 未连接
offline,
/// 加载中(进房、拉流等)
loading,
/// 错误状态(拉流失败等)
error,
}
class VideoSurface extends StatelessWidget {
final VideoState state;
final RoomUserDto user;
final Widget child;
const VideoSurface({super.key, this.state = VideoState.normal});
const VideoSurface({
super.key,
required this.user,
required this.child,
});
@override
Widget build(BuildContext context) {
String stateText = switch (state) {
VideoState.closed => "摄像头已关闭",
VideoState.offline => "掉线",
VideoState.loading => "加载中",
VideoState.error => "错误",
_ => "未知",
};
//如果不是正常
if (state != VideoState.normal) {
//摄像头是否关闭
if (user.cameraStatus == 0) {
return Align(
child: Text(stateText, style: TextStyle(color: Colors.white70)),
child: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 70,
),
child: AspectRatio(
aspectRatio: 1,
child: Container(
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 1),
),
child: CachedNetworkImage(
imageUrl: user.avatar,
fit: BoxFit.cover,
),
),
),
),
);
}
return Container();
if (user.online == 0) {
return _empty('暂时离开');
}
return child;
}
Widget _empty(String title) {
return Center(
child: Text(title, style: TextStyle(color: Colors.white70)),
);
}
}