refactor: unify error feedback system with FeedbackService and StatusBar widget

- Create FeedbackService as centralized sound/vibration dispatcher
- Create reusable StatusBar widget with StatusDotColor enum (blue/orange/green/red/yellow/amber)
- Extend SoundService with beep/error/alert sound types and loop playback
- Remove top feedback banners from both registration and boxing pages
- Route all feedback through bottom status bar per PRD requirements
- Add sound and vibration feedback to boxing module (was completely missing)
- Handle network errors with yellow status, general errors with red
- Enhance duplicate dialog with alert loop sound that stops on close
- Add beep/error/alert sound file selectors in settings page

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Misaka_Company
2026-05-12 17:15:28 +08:00
parent ac77294ecb
commit 1417c8baf9
7 changed files with 1068 additions and 517 deletions

View File

@@ -20,6 +20,9 @@ class _SettingsPageState extends State<SettingsPage> {
String? _successPath;
String? _failurePath;
String? _beepPath;
String? _errorPath;
String? _alertPath;
@override
void initState() {
@@ -35,12 +38,20 @@ class _SettingsPageState extends State<SettingsPage> {
}
Future<void> _loadSoundPaths() async {
final s = await _soundService.getSuccessPath();
final f = await _soundService.getFailurePath();
final results = await Future.wait([
_soundService.getSuccessPath(),
_soundService.getFailurePath(),
_soundService.getBeepPath(),
_soundService.getErrorPath(),
_soundService.getAlertPath(),
]);
if (mounted) {
setState(() {
_successPath = s;
_failurePath = f;
_successPath = results[0];
_failurePath = results[1];
_beepPath = results[2];
_errorPath = results[3];
_alertPath = results[4];
});
}
}
@@ -74,38 +85,66 @@ class _SettingsPageState extends State<SettingsPage> {
}
}
Future<void> _pickSound(bool isSuccess) async {
Future<void> _pickSound(String key) async {
final result = await FilePicker.pickFiles(
type: FileType.audio,
);
if (result != null && result.files.single.path != null) {
final path = result.files.single.path!;
if (isSuccess) {
await _soundService.setSuccessPath(path);
} else {
await _soundService.setFailurePath(path);
switch (key) {
case 'success':
await _soundService.setSuccessPath(path);
case 'failure':
await _soundService.setFailurePath(path);
case 'beep':
await _soundService.setBeepPath(path);
case 'error':
await _soundService.setErrorPath(path);
case 'alert':
await _soundService.setAlertPath(path);
}
setState(() {
if (isSuccess) {
_successPath = path;
} else {
_failurePath = path;
switch (key) {
case 'success':
_successPath = path;
case 'failure':
_failurePath = path;
case 'beep':
_beepPath = path;
case 'error':
_errorPath = path;
case 'alert':
_alertPath = path;
}
});
}
}
Future<void> _clearSound(bool isSuccess) async {
if (isSuccess) {
await _soundService.setSuccessPath(null);
} else {
await _soundService.setFailurePath(null);
Future<void> _clearSound(String key) async {
switch (key) {
case 'success':
await _soundService.setSuccessPath(null);
case 'failure':
await _soundService.setFailurePath(null);
case 'beep':
await _soundService.setBeepPath(null);
case 'error':
await _soundService.setErrorPath(null);
case 'alert':
await _soundService.setAlertPath(null);
}
setState(() {
if (isSuccess) {
_successPath = null;
} else {
_failurePath = null;
switch (key) {
case 'success':
_successPath = null;
case 'failure':
_failurePath = null;
case 'beep':
_beepPath = null;
case 'error':
_errorPath = null;
case 'alert':
_alertPath = null;
}
});
}
@@ -191,29 +230,67 @@ class _SettingsPageState extends State<SettingsPage> {
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
const SizedBox(height: 16),
// 上架成功铃声
// 扫码提示音
_buildSoundRow(
label: '上架成功铃声',
path: _successPath,
onPick: () => _pickSound(true),
onPreview: _successPath != null
? () => _previewSound(_successPath!)
label: '扫码提示音',
path: _beepPath,
onPick: () => _pickSound('beep'),
onPreview: _beepPath != null
? () => _previewSound(_beepPath!)
: null,
onClear: _successPath != null ? () => _clearSound(true) : null,
onClear: _beepPath != null ? () => _clearSound('beep') : null,
),
const SizedBox(height: 12),
// 上架失败铃声
// 成功铃声
_buildSoundRow(
label: '上架失败铃声',
label: '成功铃声',
path: _successPath,
onPick: () => _pickSound('success'),
onPreview: _successPath != null
? () => _previewSound(_successPath!)
: null,
onClear: _successPath != null ? () => _clearSound('success') : null,
),
const SizedBox(height: 12),
// 失败铃声
_buildSoundRow(
label: '失败铃声',
path: _failurePath,
onPick: () => _pickSound(false),
onPick: () => _pickSound('failure'),
onPreview: _failurePath != null
? () => _previewSound(_failurePath!)
: null,
onClear:
_failurePath != null ? () => _clearSound(false) : null,
onClear: _failurePath != null ? () => _clearSound('failure') : null,
),
const SizedBox(height: 12),
// 错误铃声(无效码)
_buildSoundRow(
label: '错误铃声(无效码)',
path: _errorPath,
onPick: () => _pickSound('error'),
onPreview: _errorPath != null
? () => _previewSound(_errorPath!)
: null,
onClear: _errorPath != null ? () => _clearSound('error') : null,
),
const SizedBox(height: 12),
// 警报铃声(重复上架)
_buildSoundRow(
label: '警报铃声(重复上架)',
path: _alertPath,
onPick: () => _pickSound('alert'),
onPreview: _alertPath != null
? () => _previewSound(_alertPath!)
: null,
onClear: _alertPath != null ? () => _clearSound('alert') : null,
),
],
),