Files
SkyJourney 61017f1c39 chore: 初始化脚手架 — sunny_mochi 企业级 Flutter 模板
Core 基础设施:Flavor 三包体系、Drift+SQLCipher 加密数据库
7 拦截器 Dio 网络栈、CrashReporter 崩溃日志、Sealed Failure 错误体系
5 色板主题系统、Auth 认证骨架(Clean Architecture)、Dev Panel、Mock Adapter

通过验证:flutter analyze(0 errors)、flutter test(全绿)、staging APK 74.8MB
2026-05-14 12:51:05 +08:00

207 lines
6.3 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:sunny_mochi/core/crash/crash_datasource.dart';
import 'package:sunny_mochi/core/crash/pending_crash_report.dart';
import 'package:sunny_mochi/core/extensions/context_x.dart';
import 'package:sunny_mochi/core/widgets/app_toast.dart';
/// 崩溃报告对话框 — 下次启动时强制展示。
///
/// 用户体验:
/// - "发送报告"POST 上报 → 关闭弹窗,后台继续正常启动
/// - "跳过":直接关闭弹窗,继续正常启动
class CrashDialog extends ConsumerStatefulWidget {
const CrashDialog({super.key, required this.report});
final PendingCrashReport report;
@override
ConsumerState<CrashDialog> createState() => _CrashDialogState();
}
class _CrashDialogState extends ConsumerState<CrashDialog> {
bool _sending = false;
bool _showDetail = false;
Future<void> _send() async {
setState(() => _sending = true);
try {
await ref
.read(crashDatasourceProvider)
.sendCrashReport(widget.report);
if (mounted) {
Navigator.of(context).pop();
context.showToast('崩溃报告已发送,感谢您的反馈', type: ToastType.success);
}
} catch (e, st) {
if (mounted) {
setState(() => _sending = false);
context.showError(ref, e, st);
}
}
}
@override
Widget build(BuildContext context) {
final report = widget.report;
final time = report.occurredAtLocal;
final timeStr = time == null
? '未知时间'
: '${time.year}-${time.month.toString().padLeft(2, '0')}-'
'${time.day.toString().padLeft(2, '0')} '
'${time.hour.toString().padLeft(2, '0')}:'
'${time.minute.toString().padLeft(2, '0')}';
return AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.r),
),
title: Row(
children: [
Icon(
Icons.warning_amber_rounded,
color: const Color(0xFFFF9F0A),
size: 22.sp,
),
SizedBox(width: 8.w),
Text(
'上次发生了意外退出',
style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.w600),
),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'应用在 $timeStr 崩溃退出。发送崩溃报告有助于我们修复问题,报告不包含您的个人数据。',
style: TextStyle(
fontSize: 14.sp,
color: const Color(0xFF636366),
height: 1.5,
),
),
SizedBox(height: 12.h),
_InfoRow('设备', '${report.deviceModel} / ${report.osVersion}'),
_InfoRow('版本', '${report.appVersion}+${report.appBuild}'),
SizedBox(height: 8.h),
GestureDetector(
onTap: () => setState(() => _showDetail = !_showDetail),
child: Row(
children: [
Text(
'错误详情',
style: TextStyle(
fontSize: 13.sp,
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.w500,
),
),
Icon(
_showDetail
? Icons.expand_less_rounded
: Icons.expand_more_rounded,
size: 18.sp,
color: Theme.of(context).colorScheme.primary,
),
],
),
),
if (_showDetail) ...[
SizedBox(height: 8.h),
Container(
constraints: BoxConstraints(maxHeight: 160.h),
decoration: BoxDecoration(
color: const Color(0xFFF2F2F7),
borderRadius: BorderRadius.circular(8.r),
),
child: SingleChildScrollView(
padding:
EdgeInsets.symmetric(horizontal: 10.w, vertical: 8.h),
child: Text(
'${report.error}\n\n${_shortStack(report.stackTrace)}',
style: TextStyle(
fontSize: 11.sp,
color: const Color(0xFF3A3A3C),
fontFamily: 'monospace',
height: 1.4,
),
),
),
),
],
],
),
actions: [
TextButton(
onPressed: _sending ? null : () => Navigator.of(context).pop(),
child: Text(
'跳过',
style: TextStyle(
color: const Color(0xFF8E8E93),
fontSize: 15.sp,
),
),
),
FilledButton(
onPressed: _sending ? null : _send,
child: _sending
? SizedBox(
width: 16.w,
height: 16.w,
child: const CircularProgressIndicator.adaptive(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation(Colors.white),
),
)
: Text('发送报告', style: TextStyle(fontSize: 15.sp)),
),
],
);
}
String _shortStack(String stack) {
final lines = stack.split('\n');
if (lines.length <= 20) return stack;
return '${lines.take(20).join('\n')}\n...(${lines.length - 20} more lines)';
}
}
class _InfoRow extends StatelessWidget {
const _InfoRow(this.label, this.value);
final String label;
final String value;
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.only(bottom: 4.h),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 36.w,
child: Text(
label,
style: TextStyle(
fontSize: 12.sp,
color: const Color(0xFFAEAEB2),
),
),
),
Expanded(
child: Text(
value,
style: TextStyle(
fontSize: 12.sp,
color: const Color(0xFF636366),
),
),
),
],
),
);
}
}