import 'dart:async'; import 'dart:io'; import 'dart:developer' as dev; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:sqflite/sqflite.dart'; import 'package:path_provider/path_provider.dart'; import 'package:path/path.dart' as path; import 'package:device_info_plus/device_info_plus.dart'; //import 'package:shared_preferences/shared_preferences.dart'; import '../models/device_matching.dart'; import '../utils/charge_utils.dart'; import '../widgets/charge_detail_page.dart'; import '../services/saf_storage_service.dart'; import '../models/saf_permission_state.dart'; // --- 潘通色定义 --- const Color kHealthGreen = Color(0xFF00B140); const Color kHealthYellow = Color(0xFFEFDF00); const Color kHealthOrange = Color(0xFFD75A16); class BatteryHealthPage extends StatefulWidget { const BatteryHealthPage({super.key}); @override State createState() => _BatteryHealthPageState(); } class _BatteryHealthPageState extends State with SingleTickerProviderStateMixin { // ---------- 数据 ---------- double _currentCap = 0; int _cycleCount = 0; double _batteryTemp = 0; double _batteryVolt = 0; int _designCap = 5050; String _deviceModel = "魅族21 Pro (默认)"; double _maxTemp = 0; double _minTemp = 0; double _maxVolt = 0; double _minVolt = 0; bool _hasData = false; // ---------- 趋势图数据 ---------- List> _trendData = []; // ---------- 充电状态 ---------- bool _isCharging = false; String _chargeType = ''; double _chargePower = 0.0; double _chargeVolt = 0.0; double _chargeCurr = 0.0; int? _wiredLevel; bool _isWireless = false; DateTime? _chargingStartTime; // ---------- SAF 状态 ---------- final SafStorageService _safService = SafStorageService(); SafPermissionState _safState = SafPermissionState.unauthorized; // 动画控制 late AnimationController _chargeAnimationController; late Animation _bottomSlide; late Animation _entryFadeIn; // 平台通道 static const _methodChannel = MethodChannel('com.lxh.battery_health/charge'); static const _eventChannel = EventChannel( 'com.lxh.battery_health/charge_events', ); StreamSubscription? _chargeEventSubscription; // 轮询定时器(15秒) Timer? _pollTimer; // ---------- 数据库 ---------- Database? _db; Future get database async { if (_db != null) return _db!; _db = await _initDatabase(); return _db!; } Future _initDatabase() async { dev.log('🔵 _initDatabase 开始', name: 'BatteryHealth'); Directory appDocDir = await getApplicationDocumentsDirectory(); String dbPath = path.join(appDocDir.path, 'battery_health.db'); dev.log('🔵 数据库路径: $dbPath', name: 'BatteryHealth'); return await openDatabase( dbPath, version: 2, onCreate: (db, version) async { await db.execute( 'CREATE TABLE battery_history (' 'date TEXT PRIMARY KEY, ' 'capacity INTEGER, ' 'volt_max REAL, ' 'volt_min REAL' ')', ); }, onUpgrade: (db, oldVersion, newVersion) async { if (oldVersion < 2) { try { await db.execute( 'ALTER TABLE battery_history ADD COLUMN volt_max REAL', ); await db.execute( 'ALTER TABLE battery_history ADD COLUMN volt_min REAL', ); } catch (_) {} } }, ); } Future _insertOrUpdate( String date, int capacity, double voltMax, double voltMin, ) async { Database db = await database; await db.insert('battery_history', { 'date': date, 'capacity': capacity, 'volt_max': voltMax, 'volt_min': voltMin, }, conflictAlgorithm: ConflictAlgorithm.replace); } Future>> _queryRecentData(int days) async { Database db = await database; return await db.query('battery_history', orderBy: 'date DESC', limit: days); } // ---------- 设备信息 ---------- Future _getDeviceInfo() async { dev.log('🔵 _getDeviceInfo 开始', name: 'BatteryHealth'); DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); try { AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; String model = androidInfo.model; dev.log('🔵 设备型号: $model', name: 'BatteryHealth'); setState(() { _deviceModel = DeviceMatching.getModelName(model); _designCap = DeviceMatching.getDesignCapacity(model); }); } catch (e) { dev.log('🔵 获取设备信息失败: $e', name: 'BatteryHealth'); setState(() { _deviceModel = '魅族21 Pro (默认)'; _designCap = 5050; }); } } // ---------- 扫描目录 (SAF) ---------- Future _scanAndUpdateDB() async { dev.log('🔵 _scanAndUpdateDB 开始', name: 'BatteryHealth'); _safState = await _safService.getPermissionState(); if (_safState != SafPermissionState.authorized) { dev.log('🔵 _scanAndUpdateDB: 未授权', name: 'BatteryHealth'); return; } try { final files = await _safService.listFiles(); dev.log('🔵 扫描文件列表: $files', name: 'BatteryHealth'); RegExp filePattern = RegExp(r'mbattery_charger_log_(\d{4}_\d{2}_\d{2})$'); for (String fileName in files) { Match? match = filePattern.firstMatch(fileName); if (match != null) { String dateStr = match.group(1)!.replaceAll('_', '-'); dev.log('🔵 处理文件: $fileName, 日期: $dateStr', name: 'BatteryHealth'); try { List lines = await _safService.readLogFile(fileName); if (lines.isEmpty) continue; List validLines = lines .where((line) => line.contains('tbat=')) .toList(); if (validLines.isEmpty) continue; String lastLine = validLines.last; RegExp capReg = RegExp(r'learned_cap=(\d+)'); String? capStr = capReg.firstMatch(lastLine)?.group(1); if (capStr == null) continue; int cap = int.parse(capStr) ~/ 1000; RegExp voltReg = RegExp(r'vbat=(\d+)'); double? maxVolt, minVolt; for (String line in validLines) { Match? match = voltReg.firstMatch(line); if (match != null) { double v = int.parse(match.group(1)!) / 1000000; if (maxVolt == null || v > maxVolt) maxVolt = v; if (minVolt == null || v < minVolt) minVolt = v; } } if (maxVolt == null || minVolt == null) continue; await _insertOrUpdate(dateStr, cap, maxVolt, minVolt); dev.log('🔵 插入数据库: $dateStr, cap=$cap', name: 'BatteryHealth'); } catch (e) { dev.log('🔵 处理文件 $fileName 异常: $e', name: 'BatteryHealth'); } } } } catch (e) { dev.log('🔵 _scanAndUpdateDB 异常: $e', name: 'BatteryHealth'); } } // ---------- UTC 文件名 ---------- String getTodayFileName() { DateTime nowUtc = DateTime.now().toUtc(); String year = nowUtc.year.toString(); String month = nowUtc.month.toString().padLeft(2, '0'); String day = nowUtc.day.toString().padLeft(2, '0'); String fileName = 'mbattery_charger_log_${year}_${month}_$day'; dev.log('🔵 getTodayFileName: $fileName', name: 'BatteryHealth'); return fileName; } // ---------- 时间戳工具方法 ---------- DateTime? _parseLogTimestamp(String line) { RegExp reg = RegExp(r'\[(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2})\]'); Match? match = reg.firstMatch(line); if (match == null) return null; try { String dateStr = match.group(1)!; String timeStr = match.group(2)!; List dateParts = dateStr.split('-'); List timeParts = timeStr.split(':'); return DateTime.utc( int.parse(dateParts[0]), int.parse(dateParts[1]), int.parse(dateParts[2]), int.parse(timeParts[0]), int.parse(timeParts[1]), int.parse(timeParts[2]), ); } catch (_) { return null; } } bool _isLogTimestampValid(DateTime? logTime) { if (logTime == null) return false; if (_chargingStartTime == null) { DateTime nowUtc = DateTime.now().toUtc(); Duration diff = nowUtc.difference(logTime); return diff.abs().inMinutes <= 3; } DateTime endTime = _chargingStartTime!.add(const Duration(minutes: 3)); return logTime.isAfter(_chargingStartTime!) && logTime.isBefore(endTime); } // ---------- SAF 授权引导 ---------- void _showSafAuthorizationDialog() { dev.log('🔵 _showSafAuthorizationDialog 被调用', name: 'BatteryHealth'); if (!mounted) return; showDialog( context: context, barrierDismissible: false, builder: (context) => AlertDialog( title: const Text('需要文件夹访问权限'), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: const [ Text('请授权访问日志文件夹,以读取电池数据。'), SizedBox(height: 8), Text( '目标路径:\n/storage/emulated/0/mbattery_charger/', style: TextStyle(fontSize: 12, color: Colors.grey), ), SizedBox(height: 8), Text('点击"选择文件夹"后,请在系统文件管理器中找到并选中上述文件夹。'), ], ), actions: [ TextButton( onPressed: () { dev.log('🔵 用户取消授权', name: 'BatteryHealth'); Navigator.pop(context, 'cancel'); setState(() { _safState = SafPermissionState.denied; }); }, child: const Text('取消'), ), ElevatedButton( onPressed: () { dev.log('🔵 用户点击选择文件夹', name: 'BatteryHealth'); Navigator.pop(context, 'select'); }, child: const Text('选择文件夹'), ), ], ), ).then((result) async { dev.log('🔵 对话框关闭,result: $result', name: 'BatteryHealth'); if (result == 'select') { dev.log('🔵 开始请求授权', name: 'BatteryHealth'); final authResult = await _safService.requestPermission(); dev.log( '🔵 授权结果: success=${authResult.success}, state=${authResult.state}', name: 'BatteryHealth', ); if (!mounted) return; if (authResult.success) { dev.log('🔵 授权成功,调用 readBatteryCapacity', name: 'BatteryHealth'); setState(() { _safState = SafPermissionState.authorized; }); readBatteryCapacity(); } else { dev.log('🔵 授权失败: ${authResult.errorMessage}', name: 'BatteryHealth'); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(authResult.errorMessage ?? '授权失败,请重试'), backgroundColor: Colors.red, ), ); _showSafAuthorizationDialog(); } } }); } // ---------- 核心:系统API定调 ---------- void _applySystemChargeState(bool isCharging, String systemChargeType) { dev.log( '[_applySystemChargeState] isCharging: $isCharging, systemChargeType: $systemChargeType', name: 'BatteryHealth', ); setState(() { _isCharging = isCharging; if (!isCharging) { dev.log('[_applySystemChargeState] 充电断开,重置状态', name: 'BatteryHealth'); _chargeType = ''; _isWireless = false; _chargePower = 0.0; _chargeVolt = 0.0; _chargeCurr = 0.0; _wiredLevel = null; _chargingStartTime = null; _updateChargeState(); return; } if (_chargingStartTime == null) { _chargingStartTime = DateTime.now().toUtc(); dev.log( '[_applySystemChargeState] 充电开始时间: $_chargingStartTime', name: 'BatteryHealth', ); } if (systemChargeType == "WIRELESS") { dev.log('[_applySystemChargeState] 无线充电模式', name: 'BatteryHealth'); _isWireless = true; _chargeType = "无线"; _wiredLevel = null; } else if (systemChargeType == "AC") { dev.log( '[_applySystemChargeState] 有线快充模式 (AC/PPS)', name: 'BatteryHealth', ); _isWireless = false; _chargeType = "PPS"; } else if (systemChargeType == "USB") { dev.log( '[_applySystemChargeState] 有线慢充模式 (USB/SDP)', name: 'BatteryHealth', ); _isWireless = false; _chargeType = "SDP"; } else { dev.log( '[_applySystemChargeState] ⚠️ 未知类型: $systemChargeType,清空类型等待日志', name: 'BatteryHealth', ); _isWireless = false; _chargeType = ''; } _chargePower = 0.0; _chargeVolt = 0.0; _chargeCurr = 0.0; _updateChargeState(); }); if (_isCharging) { _fetchChargeDataFromLog(); } } // ---------- 日志解析 ---------- void _parseChargeDataFromLog(List lines) { dev.log( '[_parseChargeDataFromLog] 开始解析,共 ${lines.length} 行', name: 'BatteryHealth', ); List> validLines = []; for (String line in lines) { DateTime? timestamp = _parseLogTimestamp(line); if (!_isLogTimestampValid(timestamp)) continue; bool isUsb = line.contains('chg_usb') && (line.contains('ibus=') || line.contains('real_type=')); bool isWls = line.contains('chg_wls') && line.contains('wls_online=1'); if (isUsb || isWls) { validLines.add({ 'line': line, 'timestamp': timestamp, 'type': isWls ? 'wls' : 'usb', }); } } if (validLines.isEmpty) { dev.log('[_parseChargeDataFromLog] 无有效时间窗口内的数据行', name: 'BatteryHealth'); return; } validLines.sort((a, b) => b['timestamp'].compareTo(a['timestamp'])); Map latest = validLines.first; String latestLine = latest['line']; String latestType = latest['type']; DateTime latestTime = latest['timestamp']; dev.log( '[_parseChargeDataFromLog] 最新有效数据: $latestType, 时间: $latestTime', name: 'BatteryHealth', ); // ---------- 无线 ---------- if (latestType == 'wls') { dev.log('[_parseChargeDataFromLog] 处理无线数据', name: 'BatteryHealth'); RegExp typeReg = RegExp(r'wls_type=(\w+)'); RegExp voltReg = RegExp(r'wls_volt=(\d+)'); RegExp currReg = RegExp(r'wls_curr=(\d+)'); String? wlsType = typeReg.firstMatch(latestLine)?.group(1); int volt = int.parse(voltReg.firstMatch(latestLine)?.group(1) ?? '0'); int curr = int.parse(currReg.firstMatch(latestLine)?.group(1) ?? '0'); setState(() { _isWireless = true; _chargeType = (wlsType != null && wlsType.isNotEmpty) ? wlsType : "无线"; _chargePower = (volt * curr) / 1000000.0; _chargeVolt = volt / 1000.0; _chargeCurr = curr / 1000.0; _wiredLevel = null; dev.log( '[_parseChargeDataFromLog] 无线覆盖成功: $_chargeType, ${_chargePower.toStringAsFixed(2)}W', name: 'BatteryHealth', ); }); return; } // ---------- 有线 ---------- if (latestType == 'usb') { dev.log('[_parseChargeDataFromLog] 处理有线数据', name: 'BatteryHealth'); RegExp ibusReg = RegExp(r'ibus=(\d+)'); RegExp vbusReg = RegExp(r'vbus=(\d+)'); RegExp levelReg = RegExp(r'wired_level=(\d+)'); RegExp typeReg = RegExp(r'real_type=(\w+)'); int ibus = int.parse(ibusReg.firstMatch(latestLine)?.group(1) ?? '0'); int vbus = int.parse(vbusReg.firstMatch(latestLine)?.group(1) ?? '0'); String? realType = typeReg.firstMatch(latestLine)?.group(1); int level = int.parse(levelReg.firstMatch(latestLine)?.group(1) ?? '0'); setState(() { _chargePower = (vbus * ibus) / 1000000000.0; _chargeVolt = vbus / 1000000.0; _chargeCurr = ibus / 1000000.0; _isWireless = false; if (realType != null && realType == "PPS") { _chargeType = "PPS"; } else if (realType != null && (realType == "SDP" || realType == "FLOAT")) { _chargeType = "SDP"; } else if (level >= 3 && _chargeType != "PPS") { _chargeType = "PPS"; } else if (_chargeType.isEmpty) { _chargeType = "SDP"; } if (_chargeType == "PPS") { _wiredLevel = level; } else { _wiredLevel = null; } dev.log( '[_parseChargeDataFromLog] 有线更新: $_chargeType, ${_chargePower.toStringAsFixed(2)}W, level: $level', name: 'BatteryHealth', ); }); return; } } // ---------- SAF 文件读取 ---------- Future _fetchChargeDataFromLog() async { if (!_isCharging) { dev.log('[_fetchChargeDataFromLog] 未充电,跳过读取', name: 'BatteryHealth'); return; } if (_chargingStartTime != null) { Duration elapsed = DateTime.now().toUtc().difference(_chargingStartTime!); if (elapsed.inMinutes > 5) { dev.log( '[_fetchChargeDataFromLog] 充电开始已超过 5 分钟,跳过本次轮询', name: 'BatteryHealth', ); return; } } _safState = await _safService.getPermissionState(); if (_safState != SafPermissionState.authorized) { dev.log('[_fetchChargeDataFromLog] 未授权,跳过', name: 'BatteryHealth'); return; } final fileName = getTodayFileName(); dev.log('[_fetchChargeDataFromLog] 尝试读取: $fileName', name: 'BatteryHealth'); final exists = await _safService.logFileExists(fileName); if (!exists) { dev.log( '[_fetchChargeDataFromLog] ⚠️ 文件不存在: $fileName', name: 'BatteryHealth', ); return; } try { final lines = await _safService.readLogFile(fileName); if (lines.isEmpty) { dev.log('[_fetchChargeDataFromLog] 文件为空', name: 'BatteryHealth'); return; } dev.log( '[_fetchChargeDataFromLog] ✅ 读取成功,共 ${lines.length} 行', name: 'BatteryHealth', ); _parseChargeDataFromLog(lines); } catch (e) { dev.log('[_fetchChargeDataFromLog] ❌ 读取异常: $e', name: 'BatteryHealth'); } } // ---------- 显示辅助 ---------- String _getDisplayPower() { if (!_isCharging) return ''; if (_isWireless) { return ChargeUtils.getDisplayPowerWireless(_chargePower); } else { return ChargeUtils.getDisplayPowerWired( _chargePower, _wiredLevel, _chargeType, ); } } String _getChargeTypeLabel() { return ChargeUtils.getChargeTypeLabel(_isWireless, _chargeType); } // ---------- 动画控制 ---------- void _startChargeAnimation() { if (_chargeAnimationController.isAnimating) return; _chargeAnimationController.forward(); } void _stopChargeAnimation() { if (_chargeAnimationController.isAnimating) return; _chargeAnimationController.reverse(); } void _updateChargeState() { if (!mounted) return; if (_isCharging) { _startChargeAnimation(); } else { _stopChargeAnimation(); } } // ---------- 平台通道 ---------- Future _getInitialChargeState() async { try { final Map result = await _methodChannel.invokeMethod( 'getChargeState', ); bool isCharging = result['isCharging'] ?? false; String systemType = result['chargeType'] ?? 'NONE'; _applySystemChargeState(isCharging, systemType); } catch (e) { // 静默降级 } } // ---------- 读取当前数据 ---------- Future readBatteryCapacity() async { dev.log( '🔵 ========== readBatteryCapacity 开始 ==========', name: 'BatteryHealth', ); // 检查 SAF 授权 _safState = await _safService.getPermissionState(); dev.log('🔵 SAF状态: $_safState', name: 'BatteryHealth'); if (_safState != SafPermissionState.authorized) { dev.log('🔵 未授权,弹窗', name: 'BatteryHealth'); if (mounted) _showSafAuthorizationDialog(); return; } await _getDeviceInfo(); await _scanAndUpdateDB(); final fileName = getTodayFileName(); dev.log('🔵 今日文件名: $fileName', name: 'BatteryHealth'); final exists = await _safService.logFileExists(fileName); dev.log('🔵 文件存在: $exists', name: 'BatteryHealth'); if (!exists) { dev.log('🔵 文件不存在,尝试从数据库读取', name: 'BatteryHealth'); List> recent = await _queryRecentData(1); if (recent.isNotEmpty) { dev.log('🔵 数据库有数据: ${recent.first}', name: 'BatteryHealth'); setState(() { _currentCap = recent.first['capacity'].toDouble(); _maxVolt = recent.first['volt_max'] ?? 0; _minVolt = recent.first['volt_min'] ?? 0; _hasData = true; }); await _updateTrendChart(); } else { dev.log('🔵 数据库无数据', name: 'BatteryHealth'); setState(() => _hasData = false); } if (_isCharging) _fetchChargeDataFromLog(); _chargeAnimationController.reset(); _updateChargeState(); return; } try { final lines = await _safService.readLogFile(fileName); dev.log('🔵 读取到 ${lines.length} 行', name: 'BatteryHealth'); if (lines.isEmpty) { dev.log('🔵 文件为空', name: 'BatteryHealth'); setState(() => _hasData = false); if (_isCharging) _fetchChargeDataFromLog(); _chargeAnimationController.reset(); _updateChargeState(); return; } // 打印前几行 if (lines.isNotEmpty) { dev.log('🔵 第一行: ${lines.first}', name: 'BatteryHealth'); if (lines.length > 1) { dev.log('🔵 第二行: ${lines[1]}', name: 'BatteryHealth'); } } if (_isCharging) _parseChargeDataFromLog(lines); List validLines = lines .where((line) => line.contains('tbat=')) .toList(); dev.log('🔵 有效行数 (含tbat): ${validLines.length}', name: 'BatteryHealth'); if (validLines.isEmpty) { dev.log('🔵 无有效数据行', name: 'BatteryHealth'); setState(() => _hasData = false); _chargeAnimationController.reset(); _updateChargeState(); return; } String lastLine = validLines.last; dev.log('🔵 最后一行: $lastLine', name: 'BatteryHealth'); RegExp capReg = RegExp(r'learned_cap=(\d+)'); RegExp cycleReg = RegExp(r'cycle=(\d+)'); RegExp tempReg = RegExp(r'tbat=(\d+)'); RegExp voltageReg = RegExp(r'vbat=(\d+)'); String? capStr = capReg.firstMatch(lastLine)?.group(1); String? cycleStr = cycleReg.firstMatch(lastLine)?.group(1); String? tempStr = tempReg.firstMatch(lastLine)?.group(1); String? voltStr = voltageReg.firstMatch(lastLine)?.group(1); dev.log( '🔵 capStr=$capStr, cycleStr=$cycleStr, tempStr=$tempStr, voltStr=$voltStr', name: 'BatteryHealth', ); if (capStr == null) { dev.log('🔵 未找到 learned_cap', name: 'BatteryHealth'); setState(() => _hasData = false); _chargeAnimationController.reset(); _updateChargeState(); return; } double capMah = int.parse(capStr) / 1000; int cycle = int.parse(cycleStr ?? '0'); double temp = double.parse(tempStr ?? '0'); double volt = double.parse(voltStr ?? '0') / 1000000; dev.log( '🔵 解析结果: capMah=$capMah, cycle=$cycle, temp=$temp, volt=$volt', name: 'BatteryHealth', ); List temps = []; for (String line in validLines) { RegExpMatch? match = tempReg.firstMatch(line); if (match != null) temps.add(double.parse(match.group(1)!)); } double maxTemp = temps.isNotEmpty ? temps.reduce((a, b) => a > b ? a : b) : temp; double minTemp = temps.isNotEmpty ? temps.reduce((a, b) => a < b ? a : b) : temp; double? maxVolt, minVolt; for (String line in validLines) { Match? match = voltageReg.firstMatch(line); if (match != null) { double v = int.parse(match.group(1)!) / 1000000; if (maxVolt == null || v > maxVolt) maxVolt = v; if (minVolt == null || v < minVolt) minVolt = v; } } setState(() { _currentCap = capMah; _cycleCount = cycle; _batteryTemp = temp; _batteryVolt = volt; _maxTemp = maxTemp; _minTemp = minTemp; _maxVolt = maxVolt ?? volt; _minVolt = minVolt ?? volt; _hasData = true; }); dev.log( '🔵 数据更新完成: _currentCap=$_currentCap, _cycleCount=$_cycleCount', name: 'BatteryHealth', ); await _updateTrendChart(); _chargeAnimationController.reset(); _updateChargeState(); dev.log( '🔵 ========== readBatteryCapacity 完成 ==========', name: 'BatteryHealth', ); } catch (e) { dev.log('🔵 readBatteryCapacity 异常: $e', name: 'BatteryHealth'); setState(() => _hasData = false); _chargeAnimationController.reset(); _updateChargeState(); } } Future _updateTrendChart() async { List> data = await _queryRecentData(15); setState(() { _trendData = data.reversed.toList(); }); } // ---------- 生命周期 ---------- @override void initState() { super.initState(); dev.log('🔵 ========== initState 开始 ==========', name: 'BatteryHealth'); _chargeAnimationController = AnimationController( vsync: this, duration: const Duration(milliseconds: 500), ); _bottomSlide = Tween(begin: 0, end: 24).animate( CurvedAnimation( parent: _chargeAnimationController, curve: Curves.easeInOutCubic, ), ); _entryFadeIn = Tween(begin: 0, end: 1).animate( CurvedAnimation( parent: _chargeAnimationController, curve: const Interval(0.0, 0.5, curve: Curves.easeIn), ), ); _getInitialChargeState(); _chargeEventSubscription = _eventChannel.receiveBroadcastStream().listen(( event, ) { final bool isCharging = event['isCharging'] ?? false; final String systemType = event['chargeType'] ?? 'NONE'; _applySystemChargeState(isCharging, systemType); }); _pollTimer = Timer.periodic(const Duration(seconds: 15), (timer) { if (_isCharging) { _fetchChargeDataFromLog(); } }); WidgetsBinding.instance.addPostFrameCallback((_) async { dev.log('🔵 initState -> addPostFrameCallback 开始', name: 'BatteryHealth'); await _safService.init(); dev.log( '🔵 _safService.init() 完成,cachedUri: ${_safService.cachedUri}', name: 'BatteryHealth', ); _safState = await _safService.getPermissionState(); dev.log('🔵 _safState: $_safState', name: 'BatteryHealth'); if (_safState == SafPermissionState.unauthorized || _safState == SafPermissionState.expired) { dev.log('🔵 需要授权,弹窗', name: 'BatteryHealth'); if (mounted) _showSafAuthorizationDialog(); } else { dev.log('🔵 已有授权,调用 readBatteryCapacity', name: 'BatteryHealth'); readBatteryCapacity(); } }); dev.log('🔵 ========== initState 结束 ==========', name: 'BatteryHealth'); } @override void dispose() { _chargeEventSubscription?.cancel(); _pollTimer?.cancel(); _chargeAnimationController.dispose(); _db?.close(); super.dispose(); } // ---------- Build ---------- @override Widget build(BuildContext context) { return AnnotatedRegion( value: SystemUiOverlayStyle.dark, child: Scaffold( appBar: AppBar( backgroundColor: const Color.fromARGB(0, 255, 255, 255), elevation: 0, title: const Text( '电池健康度', style: TextStyle( fontSize: 24, fontWeight: FontWeight.w600, color: Colors.black, ), ), actions: [ IconButton( icon: const Icon(Icons.refresh), onPressed: readBatteryCapacity, tooltip: '刷新数据', color: Colors.black, ), ], ), body: _hasData ? _buildDataLayout() : _buildEmptyState(), ), ); } Widget _buildEmptyState() { return const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.battery_unknown, size: 64, color: Colors.grey), SizedBox(height: 16), Text('暂无数据', style: TextStyle(fontSize: 18, color: Colors.grey)), Text('点击右上角刷新尝试', style: TextStyle(fontSize: 14, color: Colors.grey)), ], ), ); } Widget _buildDataLayout() { double healthPercent = (_designCap > 0) ? (_currentCap / _designCap) * 100 : 0; String chargeSummary = ''; if (_isCharging) { String typeLabel = _getChargeTypeLabel(); String powerLabel = _getDisplayPower(); chargeSummary = '充电中 · $typeLabel · $powerLabel'; } return Padding( padding: const EdgeInsets.all(20.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( _deviceModel, style: const TextStyle( fontSize: 14, color: Colors.grey, fontWeight: FontWeight.w400, ), ), const SizedBox(height: 4), Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ Row( crossAxisAlignment: CrossAxisAlignment.baseline, textBaseline: TextBaseline.alphabetic, children: [ Text( healthPercent.toStringAsFixed(1), style: TextStyle( fontSize: 40, fontWeight: FontWeight.w500, height: 1.0, color: healthPercent < 70 ? Colors.red : Colors.black, ), ), const SizedBox(width: 2), Text( '%', style: TextStyle( fontSize: 20, fontWeight: FontWeight.w300, height: 1.0, color: healthPercent < 70 ? Colors.red : Colors.grey.shade600, ), ), ], ), const Spacer(), Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( '$_cycleCount cycles', style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w500, height: 1.2, ), ), const SizedBox(height: 4), Text.rich( TextSpan( children: [ TextSpan( text: _currentCap.toStringAsFixed(0), style: const TextStyle( fontSize: 20, fontWeight: FontWeight.w300, ), ), TextSpan( text: ' / ', style: TextStyle( fontSize: 20, fontWeight: FontWeight.w300, color: Colors.grey.shade500, ), ), TextSpan( text: '${_designCap}mAh', style: const TextStyle( fontSize: 20, fontWeight: FontWeight.w300, ), ), ], ), style: const TextStyle( fontSize: 20, fontWeight: FontWeight.w300, height: 1.0, ), ), ], ), ], ), const SizedBox(height: 24), _buildTrendChart(), const SizedBox(height: 8), AnimatedBuilder( animation: _chargeAnimationController, builder: (context, child) { double entryHeight = 28 * _entryFadeIn.value; Widget chargeEntry = Opacity( opacity: _entryFadeIn.value, child: _buildChargeEntry(chargeSummary), ); return Column( children: [ SizedBox(height: entryHeight, child: chargeEntry), Transform.translate( offset: Offset(0, _bottomSlide.value), child: _buildBottomInfo(), ), ], ); }, ), const Spacer(), ], ), ); } Widget _buildChargeEntry(String summary) { return GestureDetector( onTap: () { if (_isCharging) { Navigator.push( context, MaterialPageRoute( builder: (context) => ChargeDetailPage( chargeType: _chargeType, isWireless: _isWireless, power: _chargePower, volt: _chargeVolt, curr: _chargeCurr, wiredLevel: _wiredLevel, ), ), ); } }, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( summary, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w500, color: Colors.black87, ), ), Row( children: [ Text( '充电信息', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w500, color: Colors.grey.shade700, ), ), const SizedBox(width: 4), Text( '>', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w500, color: Colors.grey.shade700, ), ), ], ), ], ), ); } Widget _buildTrendChart() { List> displayData = _trendData.isNotEmpty ? _trendData : _getMockData(); List dates = displayData.map((e) { String date = e['date'] as String; var parts = date.split('-'); return '${parts[1]}/${parts[2]}'; }).toList(); List percents = displayData.map((e) { int cap = e['capacity'] as int; return (cap / _designCap) * 100; }).toList(); while (percents.length < 15) { percents.insert(0, 0); dates.insert(0, ''); } if (percents.length > 15) { percents = percents.sublist(percents.length - 15); dates = dates.sublist(dates.length - 15); } int todayIndex = percents.length - 1; int sevenDaysAgoIndex = todayIndex - 7; bool showSevenDaysAgo = sevenDaysAgoIndex >= 0; String sevenDaysAgoDate = showSevenDaysAgo ? dates[sevenDaysAgoIndex] : ''; String todayDate = dates[todayIndex]; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( '容量百分比变动趋势(近15天)', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500), ), const SizedBox(height: 8), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( height: 180, child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: const [ Text( '100', style: TextStyle( fontSize: 10, color: Colors.grey, height: 1.0, ), ), Text( '70', style: TextStyle( fontSize: 10, color: Colors.grey, height: 1.0, ), ), ], ), ), const SizedBox(width: 4), Expanded( child: LayoutBuilder( builder: (context, constraints) { double totalWidth = constraints.maxWidth; double step = totalWidth / 15; return SizedBox( height: 180, child: Stack( children: [ Padding( padding: const EdgeInsets.only(bottom: 20), child: Row( crossAxisAlignment: CrossAxisAlignment.end, children: List.generate(percents.length, (index) { double value = percents[index]; Color barColor; if (value > 0) { if (value >= 80) { barColor = kHealthGreen; } else if (value >= 70) { barColor = kHealthYellow; } else { barColor = Colors.red; } } else { barColor = Colors.grey.shade300; } double clampedValue = value.clamp(70.0, 100.0); double heightFactor = (clampedValue - 70.0) / 30.0; return Expanded( child: Align( alignment: Alignment.bottomCenter, child: Container( margin: const EdgeInsets.symmetric( horizontal: 2, ), height: 20 + (100 * heightFactor), width: 18, decoration: BoxDecoration( color: barColor, borderRadius: BorderRadius.circular(4), ), ), ), ); }), ), ), if (showSevenDaysAgo && sevenDaysAgoDate.isNotEmpty) Positioned( left: sevenDaysAgoIndex * step + (step / 2) - 14, bottom: 2, child: Text( sevenDaysAgoDate, style: const TextStyle( fontSize: 9, color: Colors.grey, height: 1.0, ), ), ), if (todayDate.isNotEmpty) Positioned( left: todayIndex * step + (step / 2) - 14, bottom: 2, child: Text( todayDate, style: const TextStyle( fontSize: 9, color: Colors.black, height: 1.0, fontWeight: FontWeight.w600, ), ), ), ], ), ); }, ), ), ], ), if (percents.every((v) => v == 0)) const Padding( padding: EdgeInsets.only(top: 8.0), child: Text( '暂无历史数据,请点击刷新按钮更新', style: TextStyle(fontSize: 12, color: Colors.grey), ), ), ], ); } List> _getMockData() { List mockDates = [ '2026-06-14', '2026-06-15', '2026-06-16', '2026-06-17', '2026-06-18', '2026-06-19', '2026-06-20', '2026-06-21', '2026-06-22', '2026-06-23', '2026-06-24', '2026-06-25', '2026-06-26', '2026-06-27', '2026-06-28', ]; List mockCap = [ 4635, 4632, 4628, 4625, 4620, 4618, 4615, 4610, 4608, 4605, 4600, 4598, 4595, 4590, 4585, ]; return List.generate( 15, (i) => {'date': mockDates[i], 'capacity': mockCap[i]}, ); } Widget _buildBottomInfo() { return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildInfoItem( _buildIcon('voltage'), '电压', '${_batteryVolt.toStringAsFixed(3)}V', ), const SizedBox(height: 8), Row( children: [ Text( '↑ ${_maxVolt.toStringAsFixed(3)}V', style: const TextStyle( color: Colors.red, fontSize: 14, fontWeight: FontWeight.w500, ), ), const SizedBox(width: 12), Text( '↓ ${_minVolt.toStringAsFixed(3)}V', style: const TextStyle( color: Colors.blue, fontSize: 14, fontWeight: FontWeight.w500, ), ), ], ), ], ), ), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildInfoItem( _buildIcon('temperature'), '温度', '${_batteryTemp.toStringAsFixed(0)}°C', ), const SizedBox(height: 8), Row( children: [ Text( '↑ ${_maxTemp.toStringAsFixed(0)}°C', style: const TextStyle( color: Colors.red, fontSize: 14, fontWeight: FontWeight.w500, ), ), const SizedBox(width: 12), Text( '↓ ${_minTemp.toStringAsFixed(0)}°C', style: const TextStyle( color: Colors.blue, fontSize: 14, fontWeight: FontWeight.w500, ), ), ], ), ], ), ), ], ); } Widget _buildIcon(String type) { switch (type) { case 'voltage': return SvgPicture.asset( 'assets/icons/si--lightning-fill.svg', width: 18, height: 18, colorFilter: const ColorFilter.mode(Colors.grey, BlendMode.srcIn), ); case 'temperature': return SvgPicture.asset( 'assets/icons/mdi--temperature.svg', width: 18, height: 18, colorFilter: const ColorFilter.mode(Colors.grey, BlendMode.srcIn), ); default: return const Icon(Icons.help_outline, size: 18, color: Colors.grey); } } Widget _buildInfoItem(Widget icon, String label, String value) { return Row( children: [ icon, const SizedBox(width: 6), Text(label, style: const TextStyle(fontSize: 14, color: Colors.grey)), const SizedBox(width: 6), Text( value, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500), ), ], ); } }