fix:调整视频缩略图、调整操作记录查询逻辑
This commit is contained in:
parent
335f66a0b5
commit
9fdf8377ee
@ -236,7 +236,7 @@ class DoorLockLogLogic extends BaseGetXController {
|
|||||||
lockId: state.keyInfos.value.lockId!,
|
lockId: state.keyInfos.value.lockId!,
|
||||||
lockEventType: state.dropdownValue.value,
|
lockEventType: state.dropdownValue.value,
|
||||||
pageNo: pageNo,
|
pageNo: pageNo,
|
||||||
pageSize: 100,
|
pageSize: 1000,
|
||||||
startDate: state.startDate.value,
|
startDate: state.startDate.value,
|
||||||
endDate: state.endDate.value);
|
endDate: state.endDate.value);
|
||||||
if (entity.errorCode!.codeIsSuccessful) {
|
if (entity.errorCode!.codeIsSuccessful) {
|
||||||
@ -361,6 +361,7 @@ class DoorLockLogLogic extends BaseGetXController {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> onInit() async {
|
Future<void> onInit() async {
|
||||||
|
_setWeekRange();
|
||||||
super.onInit();
|
super.onInit();
|
||||||
|
|
||||||
// 获取是否是演示模式 演示模式不获取接口
|
// 获取是否是演示模式 演示模式不获取接口
|
||||||
@ -373,6 +374,35 @@ class DoorLockLogLogic extends BaseGetXController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _setWeekRange() {
|
||||||
|
final now = DateTime.now();
|
||||||
|
|
||||||
|
// 计算当前日期是星期几(1=周一,7=周日)
|
||||||
|
int weekday = now.weekday; // 1-7
|
||||||
|
|
||||||
|
// 计算距离本周一有多少天(向后推)
|
||||||
|
// 周一: 0天, 周二: 1天, ..., 周日: 6天
|
||||||
|
int daysToSubtract = weekday - 1; // 减去1,因为周一就是基准
|
||||||
|
|
||||||
|
// 当前周的周一 00:00:00.000
|
||||||
|
DateTime startOfWeek = DateTime(now.year, now.month, now.day)
|
||||||
|
.subtract(Duration(days: daysToSubtract));
|
||||||
|
|
||||||
|
// 当前周的周日 23:59:59.999
|
||||||
|
DateTime endOfWeek = startOfWeek
|
||||||
|
.add(Duration(days: 6)) // 加6天到周日
|
||||||
|
.add(Duration(hours: 23, minutes: 59, seconds: 59, milliseconds: 999));
|
||||||
|
|
||||||
|
// 更新响应式变量
|
||||||
|
state.startDate.value = startOfWeek.millisecondsSinceEpoch;
|
||||||
|
state.endDate.value = endOfWeek.millisecondsSinceEpoch;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 可选:提供一个方法来刷新周范围(比如切换周)
|
||||||
|
void refreshWeek() {
|
||||||
|
_setWeekRange();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> onClose() async {
|
Future<void> onClose() async {
|
||||||
super.onClose();
|
super.onClose();
|
||||||
|
|||||||
@ -37,8 +37,39 @@ class DoorLockLogPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _DoorLockLogPageState extends State<DoorLockLogPage> with RouteAware {
|
class _DoorLockLogPageState extends State<DoorLockLogPage> with RouteAware {
|
||||||
|
final ScrollController _scrollController = ScrollController();
|
||||||
final DoorLockLogLogic logic = Get.put(DoorLockLogLogic());
|
final DoorLockLogLogic logic = Get.put(DoorLockLogLogic());
|
||||||
final DoorLockLogState state = Get.find<DoorLockLogLogic>().state;
|
final DoorLockLogState state = Get.find<DoorLockLogLogic>().state;
|
||||||
|
bool _isAtBottom = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_scrollController.addListener(_onScroll);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onScroll() {
|
||||||
|
final max = _scrollController.position.maxScrollExtent;
|
||||||
|
final current = _scrollController.position.pixels;
|
||||||
|
|
||||||
|
AppLog.log('current:${current}');
|
||||||
|
// 判断是否接近底部(例如 5 像素内)
|
||||||
|
if (current >= max - 5) {
|
||||||
|
if (!_isAtBottom) {
|
||||||
|
setState(() {
|
||||||
|
_isAtBottom = true;
|
||||||
|
});
|
||||||
|
print('✅ 已滑动到 timelines 列表底部!');
|
||||||
|
// 可以在这里触发加载更多、发送事件等
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (_isAtBottom) {
|
||||||
|
setState(() {
|
||||||
|
_isAtBottom = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -104,6 +135,27 @@ class _DoorLockLogPageState extends State<DoorLockLogPage> with RouteAware {
|
|||||||
Expanded(child: timeLineView())
|
Expanded(child: timeLineView())
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
floatingActionButton: Visibility(
|
||||||
|
visible: _isAtBottom,
|
||||||
|
child: FloatingActionButton(
|
||||||
|
onPressed: () {
|
||||||
|
_scrollController.animateTo(
|
||||||
|
0.0,
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
curve: Curves.easeOut,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(48.w),
|
||||||
|
),
|
||||||
|
backgroundColor: AppColors.mainColor,
|
||||||
|
child: Icon(
|
||||||
|
Icons.arrow_upward,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 48.w,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -193,39 +245,33 @@ class _DoorLockLogPageState extends State<DoorLockLogPage> with RouteAware {
|
|||||||
borderRadius: BorderRadius.circular(16.w),
|
borderRadius: BorderRadius.circular(16.w),
|
||||||
),
|
),
|
||||||
child: Obx(
|
child: Obx(
|
||||||
() => EasyRefreshTool(
|
() => state.lockLogItemList.isNotEmpty
|
||||||
onRefresh: () async {
|
? Timeline.tileBuilder(
|
||||||
logic.mockNetworkDataRequest(isRefresh: true);
|
controller: _scrollController,
|
||||||
},
|
builder: _timelineBuilderWidget(),
|
||||||
onLoad: () async {
|
theme: TimelineThemeData(
|
||||||
logic.mockNetworkDataRequest(isRefresh: false);
|
nodePosition: 0.04, //居左侧距离
|
||||||
},
|
connectorTheme: const ConnectorThemeData(
|
||||||
child: state.lockLogItemList.isNotEmpty
|
thickness: 1.0,
|
||||||
? Timeline.tileBuilder(
|
color: AppColors.greyLineColor,
|
||||||
builder: _timelineBuilderWidget(),
|
indent: 0.5,
|
||||||
theme: TimelineThemeData(
|
|
||||||
nodePosition: 0.04, //居左侧距离
|
|
||||||
connectorTheme: const ConnectorThemeData(
|
|
||||||
thickness: 1.0,
|
|
||||||
color: AppColors.greyLineColor,
|
|
||||||
indent: 0.5,
|
|
||||||
),
|
|
||||||
indicatorTheme: const IndicatorThemeData(
|
|
||||||
size: 8.0,
|
|
||||||
color: AppColors.greyLineColor,
|
|
||||||
position: 0.4,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
)
|
indicatorTheme: const IndicatorThemeData(
|
||||||
: NoData(),
|
size: 8.0,
|
||||||
),
|
color: AppColors.greyLineColor,
|
||||||
|
position: 0.4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: NoData(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
String formatTimestampToDateTimeYYYYMMDD(int timestampMs) {
|
String formatTimestampToDateTimeYYYYMMDD(int timestampMs) {
|
||||||
DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(timestampMs);
|
DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(timestampMs);
|
||||||
DateFormat formatter = DateFormat('MM${'月'.tr}dd${'日'.tr}'); // 格式:2025-08-18 14:30
|
DateFormat formatter =
|
||||||
|
DateFormat('MM${'月'.tr}dd${'日'.tr}'); // 格式:2025-08-18 14:30
|
||||||
return formatter.format(dateTime);
|
return formatter.format(dateTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -377,7 +423,6 @@ class _DoorLockLogPageState extends State<DoorLockLogPage> with RouteAware {
|
|||||||
Text(
|
Text(
|
||||||
'${formatTimestampToDateTimeYYYYMMDD(timelineData.operateDate!)}',
|
'${formatTimestampToDateTimeYYYYMMDD(timelineData.operateDate!)}',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
|
|
||||||
fontSize: 20.sp,
|
fontSize: 20.sp,
|
||||||
)),
|
)),
|
||||||
// 使用 SingleChildScrollView 实现横向滚动
|
// 使用 SingleChildScrollView 实现横向滚动
|
||||||
|
|||||||
@ -1,53 +1,47 @@
|
|||||||
import 'dart:io'; // 导入 dart:io 以使用 File 类
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:path_provider/path_provider.dart'; // 导入 path_provider
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:video_thumbnail/video_thumbnail.dart'; // 导入 video_thumbnail
|
import 'package:video_thumbnail/video_thumbnail.dart';
|
||||||
|
|
||||||
class VideoThumbnailImage extends StatefulWidget {
|
class VideoThumbnailImage extends StatefulWidget {
|
||||||
final String videoUrl;
|
final String videoUrl;
|
||||||
|
|
||||||
VideoThumbnailImage({required this.videoUrl});
|
const VideoThumbnailImage({Key? key, required this.videoUrl})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_VideoThumbnailState createState() => _VideoThumbnailState();
|
_VideoThumbnailState createState() => _VideoThumbnailState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _VideoThumbnailState extends State<VideoThumbnailImage> {
|
class _VideoThumbnailState extends State<VideoThumbnailImage> {
|
||||||
final Map<String, String> _thumbnailCache = {}; // 缩略图缓存
|
// ✅ 使用 static 缓存:所有实例共享,避免重复请求
|
||||||
late Future<String?> _thumbnailFuture; // 用于存储缩略图生成的 Future
|
static final Map<String, Future<String?>> _pendingThumbnails = {};
|
||||||
|
|
||||||
|
late Future<String?> _thumbnailFuture;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_thumbnailFuture = _generateThumbnail(); // 在 initState 中初始化 Future
|
// ✅ 如果已存在该 URL 的 Future,复用;否则创建并缓存
|
||||||
|
_thumbnailFuture = _pendingThumbnails.putIfAbsent(widget.videoUrl, () {
|
||||||
|
return _generateThumbnail(widget.videoUrl);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成缩略图
|
// 生成缩略图(只执行一次 per URL)
|
||||||
Future<String?> _generateThumbnail() async {
|
Future<String?> _generateThumbnail(String url) async {
|
||||||
try {
|
try {
|
||||||
// 检查缓存中是否已有缩略图
|
|
||||||
if (_thumbnailCache.containsKey(widget.videoUrl)) {
|
|
||||||
return _thumbnailCache[widget.videoUrl];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取临时目录路径
|
|
||||||
final tempDir = await getTemporaryDirectory();
|
final tempDir = await getTemporaryDirectory();
|
||||||
final thumbnailPath = await VideoThumbnail.thumbnailFile(
|
final thumbnail = await VideoThumbnail.thumbnailFile(
|
||||||
video: widget.videoUrl,
|
video: url,
|
||||||
// 视频 URL
|
|
||||||
thumbnailPath: tempDir.path,
|
thumbnailPath: tempDir.path,
|
||||||
// 缩略图保存路径
|
|
||||||
imageFormat: ImageFormat.JPEG,
|
imageFormat: ImageFormat.JPEG,
|
||||||
// 缩略图格式
|
|
||||||
maxHeight: 200,
|
maxHeight: 200,
|
||||||
// 缩略图最大高度
|
quality: 100,
|
||||||
quality: 100, // 缩略图质量 (0-100)
|
|
||||||
);
|
);
|
||||||
|
return thumbnail;
|
||||||
// 更新缓存
|
|
||||||
_thumbnailCache[widget.videoUrl] = thumbnailPath!;
|
|
||||||
return thumbnailPath;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Failed to generate thumbnail: $e');
|
print('Failed to generate thumbnail: $e');
|
||||||
return null;
|
return null;
|
||||||
@ -57,27 +51,25 @@ class _VideoThumbnailState extends State<VideoThumbnailImage> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return FutureBuilder<String?>(
|
return FutureBuilder<String?>(
|
||||||
future: _thumbnailFuture, // 生成缩略图的 Future
|
future: _thumbnailFuture,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
// 加载中显示转圈
|
|
||||||
return Center(child: CircularProgressIndicator());
|
return Center(child: CircularProgressIndicator());
|
||||||
} else if (snapshot.hasError || !snapshot.hasData) {
|
} else if (snapshot.hasError || !snapshot.hasData) {
|
||||||
// 加载失败或没有数据时显示提示
|
return Center(
|
||||||
return Image.asset(
|
child: Image.asset(
|
||||||
'images/icon_unHaveData.png', // 错误图片路径
|
'images/icon_unHaveData.png',
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// 加载成功,显示缩略图
|
|
||||||
final thumbnailPath = snapshot.data!;
|
|
||||||
return Stack(
|
return Stack(
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
RotatedBox(
|
RotatedBox(
|
||||||
quarterTurns: -1,
|
quarterTurns: -1,
|
||||||
child: Image.file(
|
child: Image.file(
|
||||||
File(thumbnailPath), // 显示生成的缩略图
|
File(snapshot.data!),
|
||||||
width: 200,
|
width: 200,
|
||||||
height: 200,
|
height: 200,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user