Files
kitchen/lib/src/widgets/recipe_detail/interaction/recipe_email_button.dart
2026-04-15 07:11:28 +08:00

571 lines
20 KiB
Dart
Raw 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.
// 2026-04-14 | recipe_email_button.dart | 菜谱邮件分享按钮 | 点击弹出输入邮箱对话框并发送菜谱详情
// 2026-04-14 | 初始创建,支持收件人邮箱输入+发送状态反馈
// 2026-04-14 | 新增多线路选择官方线路1/线路2/自定义SMTP
import 'package:flutter/cupertino.dart';
import 'package:mom_kitchen/src/config/design_tokens.dart';
import 'package:mom_kitchen/src/models/recipe/recipe_model.dart';
import 'package:mom_kitchen/src/services/data/email_service.dart';
import 'package:mom_kitchen/src/services/ui/toast_service.dart';
/// 发送线路枚举
enum EmailRouteType {
/// 官方线路1 (mboxhosting)
official1,
/// 官方线路2 (QQ邮箱)
official2,
/// 自定义SMTP
custom,
}
/// 菜谱邮件分享按钮
/// 在菜谱详情页底部显示,点击后弹出对话框选择线路并输入收件人邮箱
class RecipeEmailButton extends StatefulWidget {
final RecipeModel recipe;
const RecipeEmailButton({super.key, required this.recipe});
@override
State<RecipeEmailButton> createState() => _RecipeEmailButtonState();
}
class _RecipeEmailButtonState extends State<RecipeEmailButton> {
bool _isSending = false;
@override
Widget build(BuildContext context) {
final isDark = CupertinoTheme.brightnessOf(context) == Brightness.dark;
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: DesignTokens.space4,
vertical: DesignTokens.space3,
),
child: SizedBox(
width: double.infinity,
child: CupertinoButton(
onPressed: _isSending ? null : () => _showEmailDialog(context),
borderRadius: DesignTokens.borderRadiusLg,
color: isDark ? DarkDesignTokens.primary : DesignTokens.dynamicPrimary,
padding: const EdgeInsets.symmetric(
horizontal: DesignTokens.space4,
vertical: DesignTokens.space3,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_isSending
? const CupertinoActivityIndicator(color: CupertinoColors.white)
: const Icon(
CupertinoIcons.mail,
color: CupertinoColors.white,
size: 20,
),
const SizedBox(width: DesignTokens.space2),
Text(
_isSending ? '发送中...' : '📧 发送菜谱到邮箱',
style: const TextStyle(
color: CupertinoColors.white,
fontSize: DesignTokens.fontLg,
fontWeight: FontWeight.w600,
),
),
],
),
),
),
);
}
/// 显示邮件输入对话框
void _showEmailDialog(BuildContext context) {
showCupertinoModalPopup<void>(
context: context,
builder: (BuildContext dialogContext) {
return _EmailDialog(recipe: widget.recipe);
},
);
}
}
/// 邮件发送对话框StatefulWidget管理线路选择和输入状态
class _EmailDialog extends StatefulWidget {
final RecipeModel recipe;
const _EmailDialog({required this.recipe});
@override
State<_EmailDialog> createState() => _EmailDialogState();
}
class _EmailDialogState extends State<_EmailDialog> {
EmailRouteType _selectedRoute = EmailRouteType.official1;
final _recipientController = TextEditingController();
final _smtpHostController = TextEditingController();
final _smtpPortController = TextEditingController(text: '465');
final _smtpUserController = TextEditingController();
final _smtpPassController = TextEditingController();
bool _isSending = false;
@override
void dispose() {
_recipientController.dispose();
_smtpHostController.dispose();
_smtpPortController.dispose();
_smtpUserController.dispose();
_smtpPassController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final isDark = CupertinoTheme.brightnessOf(context) == Brightness.dark;
return Container(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.78,
),
padding: const EdgeInsets.only(top: DesignTokens.space2),
decoration: BoxDecoration(
color: isDark ? DarkDesignTokens.card : DesignTokens.card,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(DesignTokens.radiusXl),
),
),
child: SafeArea(
top: false,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// 拖拽指示条
Container(
width: 36,
height: 5,
margin: const EdgeInsets.symmetric(
vertical: DesignTokens.space2,
),
decoration: BoxDecoration(
color: DesignTokens.text3.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(2.5),
),
),
// 标题栏
Padding(
padding: const EdgeInsets.symmetric(
horizontal: DesignTokens.space4,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
CupertinoButton(
padding: EdgeInsets.zero,
child: Text(
'取消',
style: TextStyle(
color: isDark ? DarkDesignTokens.text2 : DesignTokens.text2,
),
),
onPressed: () => Navigator.pop(context),
),
Text(
'📧 发送菜谱',
style: TextStyle(
fontSize: DesignTokens.fontLg,
fontWeight: FontWeight.w600,
color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1,
),
),
CupertinoButton(
padding: EdgeInsets.zero,
child: _isSending
? const CupertinoActivityIndicator()
: Text(
'发送',
style: TextStyle(
color: DesignTokens.dynamicPrimary,
fontWeight: FontWeight.w600,
),
),
onPressed: _isSending ? null : _handleSend,
),
],
),
),
const SizedBox(height: DesignTokens.space2),
// 表单区域
Flexible(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(
horizontal: DesignTokens.space4,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 菜谱信息预览
_buildPreviewCard(isDark),
const SizedBox(height: DesignTokens.space3),
// 🔀 发送线路选择
_buildSectionLabel('🔀 发送线路', isDark),
const SizedBox(height: DesignTokens.space2),
..._buildRouteOptions(isDark),
const SizedBox(height: DesignTokens.space3),
// 自定义SMTP输入区仅在自定义线路时显示
if (_selectedRoute == EmailRouteType.custom) ...[
_buildSectionLabel('⚙️ SMTP 配置', isDark),
const SizedBox(height: DesignTokens.space1),
_buildTextField(
controller: _smtpHostController,
placeholder: 'SMTP 服务器(如 smtp.qq.com',
keyboardType: TextInputType.url,
isDark: isDark,
),
const SizedBox(height: DesignTokens.space2),
Row(
children: [
Expanded(
flex: 2,
child: _buildTextField(
controller: _smtpUserController,
placeholder: '邮箱账号',
keyboardType: TextInputType.emailAddress,
isDark: isDark,
),
),
const SizedBox(width: DesignTokens.space2),
Expanded(
flex: 1,
child: _buildTextField(
controller: _smtpPortController,
placeholder: '端口',
keyboardType: TextInputType.number,
isDark: isDark,
),
),
],
),
const SizedBox(height: DesignTokens.space2),
_buildTextField(
controller: _smtpPassController,
placeholder: '授权码 / 密码',
obscureText: true,
isDark: isDark,
),
const SizedBox(height: DesignTokens.space2),
Text(
'💡 需使用邮箱SMTP授权码非登录密码可在邮箱设置中获取。端口465=SSL587=STARTTLS。',
style: TextStyle(
fontSize: DesignTokens.fontXs,
color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3,
),
),
const SizedBox(height: DesignTokens.space3),
],
// 📨 收件人邮箱
_buildSectionLabel('📨 收件人邮箱', isDark),
const SizedBox(height: DesignTokens.space1),
_buildTextField(
controller: _recipientController,
placeholder: '请输入收件人邮箱地址',
keyboardType: TextInputType.emailAddress,
isDark: isDark,
),
const SizedBox(height: DesignTokens.space2),
Text(
'💡 菜谱详情食材、步骤、营养信息将以精美HTML邮件发送',
style: TextStyle(
fontSize: DesignTokens.fontXs,
color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3,
),
),
const SizedBox(height: DesignTokens.space4),
],
),
),
),
],
),
),
);
}
/// 构建线路选择选项
List<Widget> _buildRouteOptions(bool isDark) {
return EmailRouteType.values.map((route) {
final isSelected = _selectedRoute == route;
final routeInfo = route == EmailRouteType.custom
? null
: EmailService.presetRoutes[route.index];
final label = route == EmailRouteType.custom
? '🔧 自定义SMTP'
: '${routeInfo!.icon} ${routeInfo.name}';
return Padding(
padding: const EdgeInsets.only(bottom: DesignTokens.space2),
child: GestureDetector(
onTap: () => setState(() => _selectedRoute = route),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: DesignTokens.space3,
vertical: DesignTokens.space3,
),
decoration: BoxDecoration(
color: isSelected
? DesignTokens.dynamicPrimaryLight
: (isDark ? DarkDesignTokens.background : DesignTokens.background),
borderRadius: DesignTokens.borderRadiusMd,
border: Border.all(
color: isSelected
? DesignTokens.dynamicPrimary
: DesignTokens.text3.withValues(alpha: 0.15),
width: isSelected ? 1.5 : 0.5,
),
),
child: Row(
children: [
// 选中指示器
Container(
width: 20,
height: 20,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: isSelected
? DesignTokens.dynamicPrimary
: CupertinoColors.transparent,
border: Border.all(
color: isSelected
? DesignTokens.dynamicPrimary
: (isDark ? DarkDesignTokens.text3 : DesignTokens.text3),
width: 1.5,
),
),
child: isSelected
? const Icon(CupertinoIcons.checkmark, size: 12, color: CupertinoColors.white)
: null,
),
const SizedBox(width: DesignTokens.space3),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(
fontSize: DesignTokens.fontMd,
fontWeight: FontWeight.w600,
color: isSelected
? DesignTokens.dynamicPrimary
: (isDark ? DarkDesignTokens.text1 : DesignTokens.text1),
),
),
if (routeInfo != null)
Text(
'${routeInfo.username} · ${routeInfo.host}',
style: TextStyle(
fontSize: DesignTokens.fontXs,
color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3,
),
),
if (route == EmailRouteType.custom)
Text(
'使用自己的邮箱服务器发送',
style: TextStyle(
fontSize: DesignTokens.fontXs,
color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3,
),
),
],
),
),
],
),
),
),
);
}).toList();
}
/// 菜谱信息预览卡片
Widget _buildPreviewCard(bool isDark) {
return Container(
padding: const EdgeInsets.all(DesignTokens.space3),
decoration: BoxDecoration(
color: isDark ? DarkDesignTokens.background : DesignTokens.background,
borderRadius: DesignTokens.borderRadiusMd,
),
child: Row(
children: [
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: DesignTokens.dynamicPrimaryLight,
borderRadius: DesignTokens.borderRadiusSm,
),
child: const Center(
child: Text('🍳', style: TextStyle(fontSize: 24)),
),
),
const SizedBox(width: DesignTokens.space3),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.recipe.title,
style: TextStyle(
fontSize: DesignTokens.fontMd,
fontWeight: FontWeight.w600,
color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (widget.recipe.categoryName != null)
Text(
widget.recipe.categoryName!,
style: TextStyle(
fontSize: DesignTokens.fontXs,
color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3,
),
),
],
),
),
Icon(
CupertinoIcons.doc_richtext,
size: 18,
color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3,
),
],
),
);
}
/// 区域标签
Widget _buildSectionLabel(String label, bool isDark) {
return Text(
label,
style: TextStyle(
fontSize: DesignTokens.fontSm,
fontWeight: FontWeight.w600,
color: isDark ? DarkDesignTokens.text2 : DesignTokens.text2,
),
);
}
/// 统一输入框
Widget _buildTextField({
required TextEditingController controller,
required String placeholder,
required bool isDark,
TextInputType? keyboardType,
bool obscureText = false,
}) {
return CupertinoTextField(
controller: controller,
placeholder: placeholder,
placeholderStyle: TextStyle(
color: DesignTokens.text3,
fontSize: DesignTokens.fontMd,
),
style: TextStyle(
color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1,
fontSize: DesignTokens.fontMd,
),
keyboardType: keyboardType,
obscureText: obscureText,
padding: const EdgeInsets.symmetric(
horizontal: DesignTokens.space3,
vertical: DesignTokens.space3,
),
decoration: BoxDecoration(
color: isDark ? DarkDesignTokens.background : DesignTokens.background,
borderRadius: DesignTokens.borderRadiusMd,
border: Border.all(
color: DesignTokens.text3.withValues(alpha: 0.15),
width: 0.5,
),
),
);
}
/// 处理发送
Future<void> _handleSend() async {
final recipientEmail = _recipientController.text.trim();
// 校验收件人
if (recipientEmail.isEmpty) {
ToastService.warning('请输入收件人邮箱');
return;
}
if (!_isValidEmail(recipientEmail)) {
ToastService.warning('请输入正确的邮箱格式');
return;
}
// 自定义线路校验
if (_selectedRoute == EmailRouteType.custom) {
if (_smtpHostController.text.trim().isEmpty) {
ToastService.warning('请输入SMTP服务器地址');
return;
}
if (_smtpUserController.text.trim().isEmpty) {
ToastService.warning('请输入SMTP邮箱账号');
return;
}
if (_smtpPassController.text.trim().isEmpty) {
ToastService.warning('请输入SMTP授权码');
return;
}
}
setState(() => _isSending = true);
try {
bool success;
if (_selectedRoute == EmailRouteType.custom) {
success = await EmailService.sendRecipeEmailCustom(
recipientEmail: recipientEmail,
recipe: widget.recipe,
smtpHost: _smtpHostController.text.trim(),
smtpPort: int.tryParse(_smtpPortController.text.trim()) ?? 465,
smtpUsername: _smtpUserController.text.trim(),
smtpPassword: _smtpPassController.text.trim(),
);
} else {
success = await EmailService.sendRecipeEmail(
recipientEmail: recipientEmail,
recipe: widget.recipe,
routeIndex: _selectedRoute.index,
);
}
if (mounted) {
if (success) {
ToastService.success('📧 菜谱已发送到 $recipientEmail');
Navigator.pop(context);
} else {
ToastService.error('邮件发送失败,请更换线路重试');
}
}
} catch (e) {
if (mounted) {
ToastService.error('发送异常:$e');
}
} finally {
if (mounted) {
setState(() => _isSending = false);
}
}
}
/// 简单邮箱格式校验
bool _isValidEmail(String email) {
return RegExp(r'^[\w.-]+@[\w.-]+\.\w+$').hasMatch(email);
}
}