This commit is contained in:
2026-08-08 10:57:54 +08:00
parent d0d48c1ffc
commit 604af909b0
184 changed files with 12521 additions and 12722 deletions
@@ -0,0 +1,63 @@
import 'package:flutter/material.dart';
import 'package:arcx/app/app_theme.dart';
import 'package:arcx/data/local/models/models.dart';
import 'package:arcx/features/scoring/presentation/widgets/target_face_painter.dart';
import 'package:arcx/features/stats/application/stats.dart';
class HeatmapCard extends StatelessWidget {
const HeatmapCard({super.key, required this.stats});
final StatsSummary stats;
@override
Widget build(BuildContext context) {
final hasHits = stats.hitPoints.isNotEmpty;
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'落点热力图',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
const SizedBox(height: 12),
Center(
child: AspectRatio(
aspectRatio: 1,
child: hasHits
? CustomPaint(
painter: TargetFacePainter(
hitPoints: stats.hitPoints,
hitColor: AppTheme.hitPoint,
),
child: const SizedBox.expand(),
)
: CustomPaint(
painter: TargetFacePainter(
hitPoints: const [],
showAllHits: false,
),
child: const SizedBox.expand(),
),
),
),
if (!hasHits)
Padding(
padding: const EdgeInsets.only(top: 8),
child: Text(
'暂无落点数据,完成一次记分后展示',
style: TextStyle(
fontSize: 13,
color: AppTheme.textSecondary,
),
),
),
],
),
),
);
}
}
@@ -0,0 +1,93 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:arcx/app/app_theme.dart';
import 'package:arcx/core/constants/app_defaults.dart';
import 'package:arcx/data/local/models/models.dart';
import 'package:arcx/features/profile/application/profile_controller.dart';
class ProfileCard extends ConsumerWidget {
const ProfileCard({super.key, required this.onEdit});
final VoidCallback onEdit;
@override
Widget build(BuildContext context, WidgetRef ref) {
final profileAsync = ref.watch(profileProvider);
return profileAsync.when(
loading: () => const _CardShell(child: SizedBox(height: 64)),
error: (e, _) => _CardShell(child: Text('加载失败:$e')),
data: (profile) => _CardShell(
child: Row(
children: [
_Avatar(profile: profile),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
profile.nickname,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Text(
'本地用户',
style: TextStyle(
fontSize: 13,
color: AppTheme.textSecondary,
),
),
],
),
),
IconButton(
icon: const Icon(Icons.edit_outlined),
color: AppTheme.primary,
onPressed: onEdit,
),
],
),
),
);
}
}
class _Avatar extends StatelessWidget {
const _Avatar({required this.profile});
final UserProfile profile;
@override
Widget build(BuildContext context) {
final source = profile.avatarSource;
final image =
source == AvatarSource.localFile &&
profile.avatarLocalPath != null &&
profile.avatarLocalPath!.isNotEmpty
? FileImage(File(profile.avatarLocalPath!))
: const AssetImage(AppDefaults.defaultAvatarAssetPath);
return CircleAvatar(
radius: 30,
backgroundColor: AppTheme.surfaceMuted,
backgroundImage: image as ImageProvider,
);
}
}
class _CardShell extends StatelessWidget {
const _CardShell({required this.child});
final Widget child;
@override
Widget build(BuildContext context) {
return Card(
child: Padding(padding: const EdgeInsets.all(16), child: child),
);
}
}
@@ -0,0 +1,118 @@
import 'package:flutter/material.dart';
import 'package:arcx/app/app_theme.dart';
import 'package:arcx/features/stats/application/stats.dart';
class ScoreDistributionCard extends StatelessWidget {
const ScoreDistributionCard({super.key, required this.stats});
final StatsSummary stats;
@override
Widget build(BuildContext context) {
final buckets = stats.scoreDistribution;
final hasData = buckets.isNotEmpty;
final maxCount = hasData
? buckets.map((b) => b.count).reduce((a, b) => a > b ? a : b)
: 1;
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'环值分布',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
const SizedBox(height: 12),
if (!hasData)
Padding(
padding: const EdgeInsets.symmetric(vertical: 24),
child: Center(
child: Text(
'暂无数据',
style: TextStyle(
fontSize: 13,
color: AppTheme.textSecondary,
),
),
),
)
else
SizedBox(
height: 140,
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
for (var i = 0; i < buckets.length; i++) ...[
Expanded(
child: _Bar(
label: buckets[i].label,
count: buckets[i].count,
ratio: buckets[i].count / maxCount,
),
),
if (i < buckets.length - 1) const SizedBox(width: 6),
],
],
),
),
],
),
),
);
}
}
class _Bar extends StatelessWidget {
const _Bar({
required this.label,
required this.count,
required this.ratio,
});
final String label;
final int count;
final double ratio;
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final maxH = constraints.maxHeight - 28; // reserve space for label
final h = (maxH * ratio).clamp(4.0, maxH);
return Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text(
'$count',
style: TextStyle(
fontSize: 11,
color: AppTheme.textSecondary,
),
),
const SizedBox(height: 4),
Container(
width: double.infinity,
height: h,
decoration: BoxDecoration(
color: AppTheme.primary,
borderRadius: BorderRadius.circular(4),
),
),
const SizedBox(height: 4),
Text(
label,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
],
);
},
);
}
}
@@ -0,0 +1,144 @@
import 'package:flutter/material.dart';
import 'package:arcx/app/app_theme.dart';
import 'package:arcx/features/stats/application/stats.dart';
class StatsCard extends StatelessWidget {
const StatsCard({super.key, required this.stats});
final StatsSummary stats;
@override
Widget build(BuildContext context) {
final hasData = stats.hasData;
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Text(
'训练统计',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
const Spacer(),
Text(
'今日',
style: TextStyle(fontSize: 13, color: AppTheme.textSecondary),
),
],
),
const SizedBox(height: 12),
_StatGrid(
items: [
_StatItem(
label: '今日射箭',
value: hasData ? '${stats.todayArrowCount}' : '-',
unit: '',
),
_StatItem(
label: '今日消耗',
value: hasData ? stats.todayCalories.toStringAsFixed(1) : '-',
unit: '千卡',
),
_StatItem(
label: '运动强度',
value: hasData ? stats.todayIntensityLabel : '-',
unit: hasData
? stats.todayIntensityRaw.toStringAsFixed(1)
: '',
),
],
),
const Divider(height: 28),
_StatGrid(
items: [
_StatItem(
label: '训练天数',
value: hasData ? '${stats.trainingDays}' : '-',
unit: '',
),
_StatItem(
label: '累计射箭',
value: hasData ? '${stats.totalArrowCount}' : '-',
unit: '',
),
_StatItem(
label: '平均环数',
value: hasData ? stats.averageScoreLabel : '-',
unit: '',
),
],
),
],
),
),
);
}
}
class _StatGrid extends StatelessWidget {
const _StatGrid({required this.items});
final List<_StatItem> items;
@override
Widget build(BuildContext context) {
return Row(
children: [
for (var i = 0; i < items.length; i++) ...[
Expanded(child: items[i]),
if (i < items.length - 1) const SizedBox(width: 8),
],
],
);
}
}
class _StatItem extends StatelessWidget {
const _StatItem({
required this.label,
required this.value,
required this.unit,
});
final String label;
final String value;
final String unit;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(fontSize: 12, color: AppTheme.textSecondary),
),
const SizedBox(height: 4),
Row(
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
Text(
value,
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.w700,
color: AppTheme.primaryDark,
),
),
if (unit.isNotEmpty) ...[
const SizedBox(width: 2),
Text(
unit,
style: TextStyle(fontSize: 12, color: AppTheme.textSecondary),
),
],
],
),
],
);
}
}