571 lines
20 KiB
Dart
571 lines
20 KiB
Dart
// 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=SSL,587=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);
|
||
}
|
||
}
|