feat: add train location tracking functionality

This commit is contained in:
Nedifinita
2025-10-12 21:42:01 +08:00
parent 24c6abd4f3
commit 5533df92b5
5 changed files with 1410 additions and 12 deletions

View File

@@ -3,15 +3,16 @@
LBJ Console 是一款应用程序,用于通过 BLE 从 [SX1276_Receive_LBJ](https://github.com/undef-i/SX1276_Receive_LBJ) 设备接收并显示列车预警消息,功能包括:
- 接收列车预警消息,支持可选的手机推送通知。
- 监控指定列车的轨迹,在地图上显示。
- 在地图上显示预警消息的 GPS 信息。
- 基于内置数据文件显示机车配属,机车类型和车次类型。
[android](https://github.com/undef-i/LBJ_Console/tree/android) 分支包含项目早期基于 Android 平台的实现代码,已实现基本功能,现已停止开发。
## 数据文件
LBJ Console 依赖以下数据文件,位于 `assets` 目录,用于支持机车配属和车次信息的展示:
- `loco_info.csv`:包含机车配属信息,格式为 `机车型号,机车编号起始值,机车编号结束值,所属铁路局及机务段,备注`
- `loco_type_info.csv`:包含机车类型编码信息,格式为 `机车类型编码前缀,机车类型`
- `train_info.csv`:包含车次类型信息,格式为 `正则表达式,车次类型`

View File

@@ -7,6 +7,7 @@ import 'package:lbjconsole/models/train_record.dart';
import 'package:lbjconsole/screens/history_screen.dart';
import 'package:lbjconsole/screens/map_screen.dart';
import 'package:lbjconsole/screens/map_webview_screen.dart';
import 'package:lbjconsole/screens/realtime_screen.dart';
import 'package:lbjconsole/screens/settings_screen.dart';
import 'package:lbjconsole/services/ble_service.dart';
import 'package:lbjconsole/services/database_service.dart';
@@ -183,10 +184,13 @@ class _MainScreenState extends State<MainScreen> with WidgetsBindingObserver {
StreamSubscription? _connectionSubscription;
StreamSubscription? _dataSubscription;
StreamSubscription? _lastReceivedTimeSubscription;
StreamSubscription? _settingsSubscription;
DateTime? _lastReceivedTime;
bool _isHistoryEditMode = false;
final GlobalKey<HistoryScreenState> _historyScreenKey =
GlobalKey<HistoryScreenState>();
final GlobalKey<RealtimeScreenState> _realtimeScreenKey =
GlobalKey<RealtimeScreenState>();
@override
void initState() {
@@ -197,6 +201,7 @@ class _MainScreenState extends State<MainScreen> with WidgetsBindingObserver {
_initializeServices();
_checkAndStartBackgroundService();
_setupLastReceivedTimeListener();
_setupSettingsListener();
_loadMapType();
}
@@ -209,7 +214,6 @@ class _MainScreenState extends State<MainScreen> with WidgetsBindingObserver {
}
}
Future<void> _checkAndStartBackgroundService() async {
final settings = await DatabaseService.instance.getAllSettings() ?? {};
final backgroundServiceEnabled =
@@ -231,11 +235,21 @@ class _MainScreenState extends State<MainScreen> with WidgetsBindingObserver {
});
}
void _setupSettingsListener() {
_settingsSubscription =
DatabaseService.instance.onSettingsChanged((settings) {
if (mounted && _currentIndex == 1) {
_realtimeScreenKey.currentState?.loadRecords(scrollToTop: false);
}
});
}
@override
void dispose() {
_connectionSubscription?.cancel();
_dataSubscription?.cancel();
_lastReceivedTimeSubscription?.cancel();
_settingsSubscription?.cancel();
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@@ -256,6 +270,9 @@ class _MainScreenState extends State<MainScreen> with WidgetsBindingObserver {
if (_historyScreenKey.currentState != null) {
_historyScreenKey.currentState!.addNewRecord(record);
}
if (_realtimeScreenKey.currentState != null) {
_realtimeScreenKey.currentState!.addNewRecord(record);
}
});
}
@@ -302,7 +319,7 @@ class _MainScreenState extends State<MainScreen> with WidgetsBindingObserver {
backgroundColor: AppTheme.primaryBlack,
elevation: 0,
title: Text(
['列车记录', '位置地图', '设置'][_currentIndex],
['列车记录', '数据监控', '位置地图', '设置'][_currentIndex],
style: const TextStyle(
color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold),
),
@@ -394,6 +411,9 @@ class _MainScreenState extends State<MainScreen> with WidgetsBindingObserver {
onEditModeChanged: _handleHistoryEditModeChanged,
onSelectionChanged: _handleSelectionChanged,
),
RealtimeScreen(
key: _realtimeScreenKey,
),
_mapType == 'map' ? const MapScreen() : const MapWebViewScreen(),
SettingsScreen(
onSettingsChanged: () {
@@ -411,14 +431,16 @@ class _MainScreenState extends State<MainScreen> with WidgetsBindingObserver {
),
bottomNavigationBar: NavigationBar(
backgroundColor: AppTheme.secondaryBlack,
indicatorColor: AppTheme.accentBlue.withOpacity(0.2),
indicatorColor: AppTheme.accentBlue.withValues(alpha: 0.2),
selectedIndex: _currentIndex,
onDestinationSelected: (index) {
if (_currentIndex == 2 && index == 0) {
if (index == 0) {
_historyScreenKey.currentState?.reloadRecords();
}
// 如果从设置页面切换到地图页面,重新加载地图类型
if (_currentIndex == 2 && index == 1) {
if (index == 1) {
_realtimeScreenKey.currentState?.loadRecords(scrollToTop: false);
}
if (_currentIndex == 3 && index == 2) {
_loadMapType();
}
setState(() {
@@ -429,6 +451,7 @@ class _MainScreenState extends State<MainScreen> with WidgetsBindingObserver {
destinations: const [
NavigationDestination(
icon: Icon(Icons.directions_railway), label: '列车记录'),
NavigationDestination(icon: Icon(Icons.speed), label: '数据监控'),
NavigationDestination(icon: Icon(Icons.location_on), label: '位置地图'),
NavigationDestination(icon: Icon(Icons.settings), label: '设置'),
],

File diff suppressed because it is too large Load Diff

View File

@@ -232,16 +232,28 @@ class DatabaseService {
Future<int> deleteRecord(String uniqueId) async {
final db = await database;
return await db.delete(
final result = await db.delete(
trainRecordsTable,
where: 'uniqueId = ?',
whereArgs: [uniqueId],
);
if (result > 0) {
_notifyRecordDeleted([uniqueId]);
}
return result;
}
Future<int> deleteAllRecords() async {
final db = await database;
return await db.delete(trainRecordsTable);
final result = await db.delete(trainRecordsTable);
if (result > 0) {
_notifyRecordDeleted([]);
}
return result;
}
Future<int> getRecordCount() async {
@@ -279,20 +291,31 @@ class DatabaseService {
Future<int> updateSettings(Map<String, dynamic> settings) async {
final db = await database;
return await db.update(
final result = await db.update(
appSettingsTable,
settings,
where: 'id = 1',
);
if (result > 0) {
_notifySettingsChanged(settings);
}
return result;
}
Future<int> setSetting(String key, dynamic value) async {
final db = await database;
return await db.update(
final result = await db.update(
appSettingsTable,
{key: value},
where: 'id = 1',
);
if (result > 0) {
final currentSettings = await getAllSettings();
if (currentSettings != null) {
_notifySettingsChanged(currentSettings);
}
}
return result;
}
Future<List<String>> getSearchOrderList() async {
@@ -349,6 +372,42 @@ class DatabaseService {
whereArgs: [id],
);
}
_notifyRecordDeleted(uniqueIds);
}
final List<Function(List<String>)> _recordDeleteListeners = [];
final List<Function(Map<String, dynamic>)> _settingsListeners = [];
StreamSubscription<void> onRecordDeleted(Function(List<String>) listener) {
_recordDeleteListeners.add(listener);
return Stream.value(null).listen((_) {})
..onData((_) {})
..onDone(() {
_recordDeleteListeners.remove(listener);
});
}
void _notifyRecordDeleted(List<String> deletedIds) {
for (final listener in _recordDeleteListeners) {
listener(deletedIds);
}
}
StreamSubscription<void> onSettingsChanged(
Function(Map<String, dynamic>) listener) {
_settingsListeners.add(listener);
return Stream.value(null).listen((_) {})
..onData((_) {})
..onDone(() {
_settingsListeners.remove(listener);
});
}
void _notifySettingsChanged(Map<String, dynamic> settings) {
for (final listener in _settingsListeners) {
listener(settings);
}
}
Future<void> close() async {
@@ -404,6 +463,11 @@ class DatabaseService {
}
});
final currentSettings = await getAllSettings();
if (currentSettings != null) {
_notifySettingsChanged(currentSettings);
}
return true;
} catch (e) {
return false;

View File

@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 0.5.2-flutter+52
version: 0.6.0-flutter+60
environment:
sdk: ^3.5.4