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,314 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:arcx/app/app_theme.dart';
import 'package:arcx/data/local/models/models.dart';
import 'package:arcx/features/scoring/application/scoring_service.dart';
import 'package:arcx/features/scoring/presentation/point_book_edit_page.dart';
class PointBookListPage extends ConsumerStatefulWidget {
const PointBookListPage({super.key});
@override
ConsumerState<PointBookListPage> createState() => _PointBookListPageState();
}
class _PointBookListPageState extends ConsumerState<PointBookListPage> {
BowType? _bowFilter;
TargetFaceType? _targetFilter;
@override
Widget build(BuildContext context) {
final recordsAsync = ref.watch(pointRecordsProvider);
return Scaffold(
appBar: AppBar(title: const Text('计分记录')),
body: Column(
children: [
_FilterBar(
bowFilter: _bowFilter,
targetFilter: _targetFilter,
onBowChanged: (v) => setState(() => _bowFilter = v),
onTargetChanged: (v) => setState(() => _targetFilter = v),
),
Expanded(
child: recordsAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Center(child: Text('加载失败:$e')),
data: (records) {
final filtered = _applyFilters(records);
if (filtered.isEmpty) {
return _EmptyState(hasAny: records.isNotEmpty);
}
return ListView.separated(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 24),
itemCount: filtered.length,
separatorBuilder: (_, __) => const SizedBox(height: 8),
itemBuilder: (_, index) {
final record = filtered[index];
return _RecordTile(
record: record,
onTap: () => _open(context, record),
onDelete: () => _confirmDelete(context, record),
);
},
);
},
),
),
],
),
);
}
List<PointRecord> _applyFilters(List<PointRecord> records) {
var result = records;
if (_bowFilter != null) {
result = result.where((r) => r.bowType == _bowFilter).toList();
}
if (_targetFilter != null) {
result = result.where((r) => r.targetFaceType == _targetFilter).toList();
}
return result;
}
Future<void> _open(BuildContext context, PointRecord record) async {
await Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => PointBookEditPage(recordId: record.id, readOnly: true),
),
);
}
Future<void> _confirmDelete(BuildContext context, PointRecord record) async {
final ok = await showDialog<bool>(
context: context,
builder: (_) => AlertDialog(
title: const Text('删除记录'),
content: const Text('确定删除该记分记录吗?此操作不可撤销。'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('取消'),
),
FilledButton(
style: FilledButton.styleFrom(backgroundColor: Colors.red),
onPressed: () => Navigator.pop(context, true),
child: const Text('删除'),
),
],
),
);
if (ok == true) {
await ref.read(pointRecordsProvider.notifier).delete(record.id);
}
}
}
class _FilterBar extends StatelessWidget {
const _FilterBar({
required this.bowFilter,
required this.targetFilter,
required this.onBowChanged,
required this.onTargetChanged,
});
final BowType? bowFilter;
final TargetFaceType? targetFilter;
final ValueChanged<BowType?> onBowChanged;
final ValueChanged<TargetFaceType?> onTargetChanged;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
child: Row(
children: [
Expanded(
child: _Dropdown<BowType>(
label: '弓型',
value: bowFilter,
items: BowType.values,
labelOf: _bowLabel,
onChanged: onBowChanged,
),
),
const SizedBox(width: 12),
Expanded(
child: _Dropdown<TargetFaceType>(
label: '靶纸',
value: targetFilter,
items: TargetFaceType.values,
labelOf: _targetLabel,
onChanged: onTargetChanged,
),
),
],
),
);
}
}
class _Dropdown<T> extends StatelessWidget {
const _Dropdown({
required this.label,
required this.value,
required this.items,
required this.labelOf,
required this.onChanged,
});
final String label;
final T? value;
final List<T> items;
final String Function(T) labelOf;
final ValueChanged<T?> onChanged;
@override
Widget build(BuildContext context) {
return DropdownButtonFormField<T>(
value: value,
decoration: InputDecoration(
labelText: label,
isDense: true,
contentPadding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)),
),
items: [
DropdownMenuItem<T>(value: null, child: Text('全部')),
...items.map((e) => DropdownMenuItem<T>(
value: e,
child: Text(labelOf(e)),
)),
],
onChanged: onChanged,
);
}
}
class _RecordTile extends StatelessWidget {
const _RecordTile({
required this.record,
required this.onTap,
required this.onDelete,
});
final PointRecord record;
final VoidCallback onTap;
final VoidCallback onDelete;
@override
Widget build(BuildContext context) {
final isDraft = record.isDraft;
return Card(
child: ListTile(
onTap: onTap,
title: Row(
children: [
Expanded(
child: Text(
record.title,
style: const TextStyle(fontWeight: FontWeight.w600),
),
),
if (isDraft)
Container(
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: AppTheme.accent.withValues(alpha: 0.18),
borderRadius: BorderRadius.circular(6),
),
child: Text(
'草稿',
style: TextStyle(
fontSize: 11,
color: AppTheme.accent,
fontWeight: FontWeight.w600,
),
),
),
],
),
subtitle: Text(
'${_bowLabel(record.bowType)} · ${record.distanceMeters}m · ${_targetLabel(record.targetFaceType)}\n'
'${_formatDate(record.createdAt)} ${record.totalArrows}${record.averageScore != null ? '${record.averageScore!.toStringAsFixed(1)}' : ''}',
style: TextStyle(fontSize: 12, color: AppTheme.textSecondary),
),
isThreeLine: true,
trailing: IconButton(
icon: const Icon(Icons.delete_outline, color: Colors.red),
onPressed: onDelete,
),
),
);
}
}
class _EmptyState extends StatelessWidget {
const _EmptyState({required this.hasAny});
final bool hasAny;
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.history,
size: 64,
color: AppTheme.textSecondary.withValues(alpha: 0.4),
),
const SizedBox(height: 12),
Text(
hasAny ? '当前筛选条件下无记录' : '暂无记分记录',
style: TextStyle(color: AppTheme.textSecondary),
),
],
),
);
}
}
String _bowLabel(BowType t) {
switch (t) {
case BowType.recurve:
return '反曲弓';
case BowType.compound:
return '复合弓';
case BowType.barebow:
return '光弓';
case BowType.traditional:
return '传统弓';
case BowType.longbow:
return '长弓';
case BowType.other:
return '其他';
}
}
String _targetLabel(TargetFaceType t) {
switch (t) {
case TargetFaceType.cm40:
return '40cm';
case TargetFaceType.cm60:
return '60cm';
case TargetFaceType.cm80:
return '80cm';
case TargetFaceType.cm122:
return '122cm';
case TargetFaceType.vegas3Spot:
return '维加斯三联';
case TargetFaceType.vertical3Spot:
return '垂直三联';
case TargetFaceType.custom:
return '自定义';
}
}
String _formatDate(DateTime dt) {
return '${dt.year}-${dt.month.toString().padLeft(2, '0')}-${dt.day.toString().padLeft(2, '0')} '
'${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}';
}