资讯动态

Flutter + OpenHarmony 阅读数据可视化组件开发实战

发布时间:2026/9/26 4:15:59 来源:尧图企业网站定制
Flutter OpenHarmony 阅读数据可视化组件开发实战欢迎加入开源鸿蒙跨平台社区→ https://openharmonycrosplatform.csdn.net一、效果展示 运行效果预览在鸿蒙虚拟机上运行后的实际效果如下总览仪表盘 今日阅读时长2小时35分钟本周阅读天数5天总阅读字数125,680字阅读速度320字/分钟连续阅读记录15天趋势图表展示 7天阅读时长柱状图每日阅读量折线图阅读时段分布饼图月度累计对比图详细统计数据 各分类阅读占比最常阅读时间段平均每次阅读时长阅读完成率统计目标进度追踪 月度阅读目标进度条距离目标还差多少历史目标达成情况预计完成时间二、组件概述阅读统计组件是帮助用户了解自身阅读习惯、提升阅读效率的重要工具。通过数据可视化的方式直观展示阅读时长、频率、偏好等关键指标。在 OpenHarmony 环境下开发 Flutter 应用时阅读统计组件需要支持多种图表类型、时间维度筛选、目标管理等功能。三、核心功能特性✅ 多维度数据统计 - 时长、字数、次数全面覆盖✅ 丰富的图表展示 - 柱状图、折线图、饼图、环形图✅ 灵活的时间范围 - 日/周/月/年自由切换✅ 目标追踪系统 - 设定并监控阅读目标✅ 趋势分析预测 - 智能分析阅读趋势✅ 本地数据存储 - 统计数据安全持久化四、技术实现架构4.1 数据模型设计class ReadingSession { final String id; final DateTime startTime; final DateTime endTime; final int wordCount; // 阅读字数 final String category; // 分类标签 final int completionRate; // 完成率 0-100 final Duration averageSpeed; // 平均阅读速度 const ReadingSession({ required this.id, required this.startTime, required this.endTime, required this.wordCount, this.category 未分类, this.completionRate 100, required this.averageSpeed, }); Duration get duration endTime. difference(startTime); } class ReadingStatistics { final ListReadingSession sessions; final DateTime periodStart; final DateTime periodEnd; const ReadingStatistics({ required this.sessions, required this.periodStart, required this.periodEnd, }); int get totalDurationInMinutes { return sessions.fold(0, (sum, session) sum session. duration.inMinutes); } int get totalWordCount { return sessions.fold(0, (sum, session) sum session. wordCount); } double get averageDuration { if (sessions.isEmpty) return 0; return totalDurationInMinutes / sessions.length; } MapString, int getCategoryDistribution() { final distribution String, int{}; for (var session in sessions) { distribution[session. category] (distribution[session. category] ?? 0) session. wordCount; } return distribution; } Listint getDailyDurations() { // 返回每日阅读时长的列表 final dailyData int[]; var currentDay periodStart; while (!currentDay.isAfter (periodEnd)) { final daySessions sessions. where((s) s.startTime.year currentDay.year s.startTime.month currentDay.month s.startTime.day currentDay.day, ); final dayDuration daySessions.fold( 0, (sum, s) sum s. duration.inMinutes, ); dailyData.add(dayDuration); currentDay currentDay.add (const Duration(days: 1)); } return dailyData; } }4.2 阅读目标模型class ReadingGoal { final String id; final GoalType type; // 目标类型 final double targetValue; // 目标值 final double currentValue; // 当前值 final DateTime startDate; final DateTime endDate; final bool isCompleted; const ReadingGoal({ required this.id, required this.type, required this.targetValue, required this.currentValue, required this.startDate, required this.endDate, this.isCompleted false, }); double get progress { if (targetValue 0) return 0; return (currentValue / targetValue).clamp(0.0, 1.0); } int get remainingDays { final now DateTime.now(); if (now.isAfter(endDate)) return 0; return endDate.difference(now). inDays; } bool get isExpired DateTime.now ().isAfter(endDate); } enum GoalType { dailyMinutes, // 每日阅读分钟数 weeklyHours, // 每周阅读小时数 monthlyWords, // 每月阅读字数 streakDays, // 连续阅读天数 }4.3 组件属性定义class ReadingStatsWidget extends StatefulWidget { final ReadingStatistics statistics; final StatsViewMode viewMode; // 视图模式 final ChartType chartType; // 图表类型 final bool showGoals; // 显 示目标 final bool showTrends; // 显 示趋势 final Color? primaryColor; // 主色 调 final Function(String category)? onCategoryTap; const ReadingStatsWidget({ super.key, required this.statistics, this.viewMode StatsViewMode. overview, this.chartType ChartType.bar, this.showGoals true, this.showTrends true, this.primaryColor, this.onCategoryTap, }); }五、ReadingStatsWidget 核心实现5.1 数据格式化器class DataFormatter { static String formatDuration(int minutes) { if (minutes 60) { return $minutes分钟; } else { final hours minutes ~/ 60; final mins minutes % 60; if (mins 0) { return $hours小时; } else { return $hours小时$mins分钟; } } } static String formatWordCount(int count) { if (count 10000) { return ${(count / 10000). toStringAsFixed(1)}万; } else if (count 1000) { return ${(count / 1000). toStringAsFixed(1)}k; } return count.toString(); } static String formatPercentage (double value) { return ${(value * 100).toInt()} %; } static String formatSpeed (Duration speed) { final wordsPerMinute 60000 / speed.inMilliseconds; return ${wordsPerMinute.toInt ()}字/分钟; } }5.2 总览页面构建Widget _buildOverview(bool isDark) { final stats widget.statistics; final color widget. primaryColor ?? Theme.of(context). colorScheme.primary; return SingleChildScrollView( padding: const EdgeInsets.all (16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildHeader(isDark), const SizedBox(height: 20), _buildStatCards(stats, color, isDark), const SizedBox(height: 20), if (widget.showGoals) _buildGoalProgress(color, isDark), const SizedBox(height: 20), _buildQuickChart(color, isDark), const SizedBox(height: 20), if (widget.showTrends) _buildTrendSection(color, isDark), ], ), ); } Widget _buildStatCards (ReadingStatistics stats, Color color, bool isDark) { return GridView.count( crossAxisCount: 2, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), mainAxisSpacing: 12, crossAxisSpacing: 12, childAspectRatio: 1.5, children: [ _buildStatCard( icon: Icons.access_time, title: 总阅读时长, value: DataFormatter. formatDuration(stats. totalDurationInMinutes), color: Colors.blue, isDark: isDark, ), _buildStatCard( icon: Icons.menu_book, title: 总阅读字数, value: DataFormatter. formatWordCount(stats. totalWordCount), color: Colors.green, isDark: isDark, ), _buildStatCard( icon: Icons.speed, title: 平均速度, value: ${stats. averageDuration.toInt()}分/ 次, color: Colors.orange, isDark: isDark, ), _buildStatCard( icon: Icons.calendar_today, title: 阅读次数, value: ${stats.sessions. length}次, color: Colors.purple, isDark: isDark, ), ], ).animate().fadeIn().slideY (begin: 0.1); }5.3 统计卡片组件Widget _buildStatCard({ required IconData icon, required String title, required String value, required Color color, required bool isDark, }) { return Container( padding: const EdgeInsets.all (16), decoration: BoxDecoration( color: isDark ? const Color (0xFF2A2A2A) : Colors.white, borderRadius: BorderRadius. circular(12), boxShadow: [ BoxShadow( color: color.withOpacity (0.1), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: color. withOpacity(0.1), borderRadius: BorderRadius. circular(8), ), child: Icon(icon, size: 20, color: color), ), ], ), const SizedBox(height: 12), Text( value, style: TextStyle( fontSize: 20, fontWeight: FontWeight. bold, color: isDark ? Colors. white : Colors.black87, ), ), const SizedBox(height: 4), Text( title, style: TextStyle( fontSize: 13, color: isDark ? Colors. grey[400]! : Colors.grey [600]!, ), ), ], ), ); }5.4 柱状图实现Widget _buildBarChart(Color primaryColor, bool isDark) { final data widget.statistics. getDailyDurations(); final maxValue data. isNotEmpty ? data.reduce((a, b) a b ? a : b) : 1; return Container( height: 200, padding: const EdgeInsets.all (16), decoration: BoxDecoration( color: isDark ? const Color (0xFF1E1E1E) : Colors.grey [50], borderRadius: BorderRadius. circular(12), ), child: Column( children: [ Text( 近7天阅读时长, style: TextStyle( fontSize: 14, fontWeight: FontWeight. w600, color: isDark ? Colors. white : Colors.black87, ), ), const SizedBox(height: 16), Expanded( child: Row( mainAxisAlignment: MainAxisAlignment. spaceEvenly, crossAxisAlignment: CrossAxisAlignment.end, children: data.asMap(). entries.map((entry) { final index entry. key; final value entry. value; final height maxValue 0 ? (value / maxValue) * 160 : 0; return Column( mainAxisSize: MainAxisSize.min, children: [ Text( ${value}m, style: TextStyle (fontSize: 10, color: Colors. grey[500]), ), const SizedBox (height: 4), Container( width: 32, height: height, decoration: BoxDecoration( gradient: LinearGradient ( begin: Alignment. bottomCenter , end: Alignment. topCenter, colors: [primaryColo r. withOpacity (0.7), primaryColor ], ), borderRadius: BorderRadius. circular(6), ), ).animate() .fadeIn(delay: (index * 100).ms) .slideY(begin: 0. 3, end: 0, delay: (index * 100).ms), const SizedBox (height: 8), Text( _getWeekdayLabel (index), style: TextStyle (fontSize: 11, color: Colors. grey[500]), ), ], ); }).toList(), ), ), ], ), ); } String _getWeekdayLabel(int index) { final now DateTime.now(); final weekday now.subtract (Duration(days: 6 - index)). weekday; const weekdays [一, 二, 三 , 四, 五, 六, 日]; return weekdays[weekday - 1]; }5.5 环形图实现分类占比Widget _buildPieChart(Color primaryColor, bool isDark) { final distribution widget. statistics.getCategoryDistribution (); final total distribution.values. fold(0, (a, b) a b); if (total 0 || distribution. isEmpty) { return Center(child: Text(暂无 数据)); } final colors [ Colors.blue, Colors.green, Colors.orange, Colors.purple, Colors.red, Colors.teal, ]; return Container( height: 220, padding: const EdgeInsets.all (16), decoration: BoxDecoration( color: isDark ? const Color (0xFF1E1E1E) : Colors.grey [50], borderRadius: BorderRadius. circular(12), ), child: Column( children: [ Text( 阅读分类分布, style: TextStyle( fontSize: 14, fontWeight: FontWeight. w600, color: isDark ? Colors. white : Colors.black87, ), ), const SizedBox(height: 16), Expanded( child: Row( children: [ SizedBox( width: 140, height: 140, child: CustomPaint( painter: DonutChartPainter( data: distribution. entries.map((e) { return PieSegment( value: e. value. toDouble(), color: colors [distributio n.keys. toList(). indexOf(e. key) % colors. length], label: e. key, ); }).toList(), ), ), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment : CrossAxisAlignment .start, mainAxisAlignment: MainAxisAlignment. center, children: distribution. entries.asMap(). entries.map ((entry) { final index entry.key; final mapEntry entry.value; final percentage ((mapEntry. value / total) * 100). toStringAsFixed (1); return Padding( padding: const EdgeInsets. symmetric (vertical: 4), child: Row( children: [ Container( width: 10, height: 10, decorati on: BoxDecor ation( color: colors [index % colors . length ], shape: BoxSha pe. circle , ), ), const SizedBox (width: 8), Expanded( child: Text( mapEnt ry. key, overfl ow: TextOv erflow . ellips is, style: TextSt yle (fontS ize: 12, color: isDark ? Colors . white : Colors . black8 7), ), ), Text( $percen tage%, style: TextStyl e (fontSiz e: 12, color: Colors. grey [500], fontWeig ht: FontWeig ht. bold), ),

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑