319 lines
10 KiB
Dart
319 lines
10 KiB
Dart
import 'package:flutter/material.dart';
|
||
import '../../app/theme/app_colors.dart';
|
||
import '../../app/theme/app_typography.dart';
|
||
import '../../app/theme/app_spacing.dart';
|
||
import '../../app/i18n/app_localizations.dart';
|
||
import 'genex_button.dart';
|
||
|
||
/// AI操作确认弹窗组件
|
||
///
|
||
/// AI Agent 执行操作前的二次确认弹窗
|
||
/// 场景:AI帮你出售、AI帮你购买、AI帮你转赠 等需要确认的代理操作
|
||
///
|
||
/// 展示内容:
|
||
/// - AI建议的操作描述
|
||
/// - 操作详情(金额/数量/对象等)
|
||
/// - 风险提示
|
||
/// - 确认/取消按钮
|
||
class AiConfirmDialog extends StatelessWidget {
|
||
final String actionTitle;
|
||
final String actionDescription;
|
||
final List<AiConfirmDetail> details;
|
||
final String? riskWarning;
|
||
final String? confirmText;
|
||
final String? cancelText;
|
||
final VoidCallback onConfirm;
|
||
final VoidCallback? onCancel;
|
||
final AiConfirmLevel level;
|
||
|
||
const AiConfirmDialog({
|
||
super.key,
|
||
required this.actionTitle,
|
||
required this.actionDescription,
|
||
required this.details,
|
||
this.riskWarning,
|
||
this.confirmText,
|
||
this.cancelText,
|
||
required this.onConfirm,
|
||
this.onCancel,
|
||
this.level = AiConfirmLevel.normal,
|
||
});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Dialog(
|
||
backgroundColor: Colors.transparent,
|
||
insetPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||
child: Container(
|
||
decoration: BoxDecoration(
|
||
color: AppColors.surface,
|
||
borderRadius: AppSpacing.borderRadiusLg,
|
||
boxShadow: const [
|
||
BoxShadow(
|
||
color: Color(0x1A000000),
|
||
blurRadius: 24,
|
||
offset: Offset(0, 8),
|
||
),
|
||
],
|
||
),
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
// Header with AI icon
|
||
_buildHeader(context),
|
||
|
||
// Body
|
||
Padding(
|
||
padding: const EdgeInsets.fromLTRB(20, 0, 20, 20),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
// Action Description
|
||
Text(
|
||
actionDescription,
|
||
style: AppTypography.bodyMedium.copyWith(
|
||
color: AppColors.textSecondary,
|
||
height: 1.5,
|
||
),
|
||
),
|
||
const SizedBox(height: 16),
|
||
|
||
// Detail Items
|
||
Container(
|
||
padding: const EdgeInsets.all(14),
|
||
decoration: BoxDecoration(
|
||
color: AppColors.gray50,
|
||
borderRadius: AppSpacing.borderRadiusMd,
|
||
),
|
||
child: Column(
|
||
children: details.asMap().entries.map((entry) {
|
||
final isLast = entry.key == details.length - 1;
|
||
final detail = entry.value;
|
||
return Column(
|
||
children: [
|
||
Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
children: [
|
||
Text(
|
||
detail.label,
|
||
style: AppTypography.bodySmall.copyWith(
|
||
color: AppColors.textSecondary,
|
||
),
|
||
),
|
||
Text(
|
||
detail.value,
|
||
style: detail.isHighlight
|
||
? AppTypography.labelMedium.copyWith(
|
||
color: AppColors.primary,
|
||
)
|
||
: AppTypography.labelMedium,
|
||
),
|
||
],
|
||
),
|
||
if (!isLast) ...[
|
||
const SizedBox(height: 10),
|
||
Divider(color: AppColors.gray200, height: 1),
|
||
const SizedBox(height: 10),
|
||
],
|
||
],
|
||
);
|
||
}).toList(),
|
||
),
|
||
),
|
||
|
||
// Risk Warning
|
||
if (riskWarning != null) ...[
|
||
const SizedBox(height: 12),
|
||
Container(
|
||
padding: const EdgeInsets.all(12),
|
||
decoration: BoxDecoration(
|
||
color: level == AiConfirmLevel.high
|
||
? AppColors.errorLight
|
||
: AppColors.warningLight,
|
||
borderRadius: AppSpacing.borderRadiusSm,
|
||
),
|
||
child: Row(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Icon(
|
||
level == AiConfirmLevel.high
|
||
? Icons.warning_amber_rounded
|
||
: Icons.info_outline_rounded,
|
||
size: 16,
|
||
color: level == AiConfirmLevel.high
|
||
? AppColors.error
|
||
: AppColors.warning,
|
||
),
|
||
const SizedBox(width: 8),
|
||
Expanded(
|
||
child: Text(
|
||
riskWarning!,
|
||
style: AppTypography.bodySmall.copyWith(
|
||
color: AppColors.gray700,
|
||
height: 1.4,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
|
||
const SizedBox(height: 20),
|
||
|
||
// Buttons
|
||
GenexButton(
|
||
label: confirmText ?? context.t('aiChat.confirmAction'),
|
||
onPressed: () {
|
||
Navigator.of(context).pop(true);
|
||
onConfirm();
|
||
},
|
||
),
|
||
const SizedBox(height: 8),
|
||
GenexButton(
|
||
label: cancelText ?? context.t('common.cancel'),
|
||
variant: GenexButtonVariant.text,
|
||
onPressed: () {
|
||
Navigator.of(context).pop(false);
|
||
onCancel?.call();
|
||
},
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildHeader(BuildContext context) {
|
||
return Container(
|
||
padding: const EdgeInsets.fromLTRB(20, 20, 20, 16),
|
||
child: Row(
|
||
children: [
|
||
// AI Avatar
|
||
Container(
|
||
width: 40,
|
||
height: 40,
|
||
decoration: BoxDecoration(
|
||
gradient: AppColors.primaryGradient,
|
||
borderRadius: AppSpacing.borderRadiusSm,
|
||
),
|
||
child: const Center(
|
||
child: Text(
|
||
'✨',
|
||
style: TextStyle(fontSize: 20),
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(width: 12),
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(context.t('aiChat.title'), style: AppTypography.labelMedium),
|
||
const SizedBox(height: 2),
|
||
Text(
|
||
actionTitle,
|
||
style: AppTypography.h3.copyWith(color: AppColors.primary),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
// Level indicator
|
||
Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||
decoration: BoxDecoration(
|
||
color: _levelColor.withValues(alpha: 0.1),
|
||
borderRadius: AppSpacing.borderRadiusFull,
|
||
),
|
||
child: Text(
|
||
_getLevelText(context),
|
||
style: TextStyle(
|
||
fontSize: 10,
|
||
fontWeight: FontWeight.w600,
|
||
color: _levelColor,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Color get _levelColor {
|
||
switch (level) {
|
||
case AiConfirmLevel.low:
|
||
return AppColors.success;
|
||
case AiConfirmLevel.normal:
|
||
return AppColors.warning;
|
||
case AiConfirmLevel.high:
|
||
return AppColors.error;
|
||
}
|
||
}
|
||
|
||
String _getLevelText(BuildContext context) {
|
||
switch (level) {
|
||
case AiConfirmLevel.low:
|
||
return context.t('aiChat.riskLow');
|
||
case AiConfirmLevel.normal:
|
||
return context.t('aiChat.riskNormal');
|
||
case AiConfirmLevel.high:
|
||
return context.t('aiChat.riskHigh');
|
||
}
|
||
}
|
||
|
||
/// 显示AI确认弹窗的便捷方法
|
||
static Future<bool?> show(
|
||
BuildContext context, {
|
||
required String actionTitle,
|
||
required String actionDescription,
|
||
required List<AiConfirmDetail> details,
|
||
String? riskWarning,
|
||
String? confirmText,
|
||
String? cancelText,
|
||
AiConfirmLevel level = AiConfirmLevel.normal,
|
||
}) {
|
||
return showDialog<bool>(
|
||
context: context,
|
||
barrierDismissible: false,
|
||
builder: (ctx) => AiConfirmDialog(
|
||
actionTitle: actionTitle,
|
||
actionDescription: actionDescription,
|
||
details: details,
|
||
riskWarning: riskWarning,
|
||
confirmText: confirmText,
|
||
cancelText: cancelText,
|
||
level: level,
|
||
onConfirm: () {},
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// AI确认弹窗的详情项
|
||
class AiConfirmDetail {
|
||
final String label;
|
||
final String value;
|
||
final bool isHighlight;
|
||
|
||
const AiConfirmDetail({
|
||
required this.label,
|
||
required this.value,
|
||
this.isHighlight = false,
|
||
});
|
||
}
|
||
|
||
/// AI操作风险等级
|
||
enum AiConfirmLevel {
|
||
/// 低风险:查看信息、获取建议等
|
||
low,
|
||
|
||
/// 需确认:购买、出售、转赠等涉及资产操作
|
||
normal,
|
||
|
||
/// 高风险:大额操作、提现到外部钱包等
|
||
high,
|
||
}
|