init
This commit is contained in:
@@ -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')}';
|
||||
}
|
||||
Reference in New Issue
Block a user