Files
xianyan/lib/features/mine/profile/presentation/learn_us_widgets.dart
Developer ae1df22732 feat: v6.10.3 多语言翻译补全 + 17项功能修复
- 引导页协议多语言支持(languageId传递)
- 登录页双书名号修复 + 注册页协议勾选
- 个人中心页面多语言(18个翻译键)
- 网络断开提示增加关闭/刷新按钮
- 了解我们:新增秋叶qy开发者 + ayk签名修改 + 贡献者精简 + 微风暴微信搜索
- iOS快捷按钮重复修复(删除Info.plist静态定义)
- 测试账号123456警告提示
- 扫码登录自动跳转(HTTP轮询+WebSocket双通道)
- 登录页老用户按钮改次要色
- Syncfusion图表崩溃修复(DeferredBuilder+animationDuration:0)
- macOS标题栏跟随软件夜间模式
- 平台兼容分发渠道弹窗
- 软件著作权图片+交叉水印
- 桌面小部件平台兼容说明默认收起
- iOS/macOS图标更新+名称确认为闲言
- 12个语言文件补全roleNative+7个分发渠道翻译字段
2026-06-02 04:50:32 +08:00

721 lines
23 KiB
Dart

/// ============================================================
/// 闲言APP — 了解我们页面(通用组件)
/// 创建时间: 2026-05-29
/// 更新时间: 2026-06-01
/// 作用: 了解我们页面的通用小组件和数据类
/// 上次更新: 重新设计TeamMemberTile卡片+社交链接;TeamMemberData新增social字段
/// ============================================================
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
import '../../../../core/theme/app_theme.dart';
import '../../../../core/theme/app_spacing.dart';
import '../../../../core/theme/app_typography.dart';
import '../../../../core/theme/app_radius.dart';
import '../../../../l10n/translations.dart';
import '../../../../shared/widgets/feedback/external_link_dialog.dart';
// ============================================================
// 通用组件 — 链接项
// ============================================================
class LinkTile extends StatelessWidget {
const LinkTile({
super.key,
required this.link,
required this.ext,
required this.t,
});
final LinkItemData link;
final AppThemeExtension ext;
final T t;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => _showLaunchDialog(context, link.url, link.label),
behavior: HitTestBehavior.opaque,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.md,
),
child: Row(
children: [
Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: ext.accent.withValues(alpha: 0.12),
borderRadius: AppRadius.smBorder,
),
child: Icon(link.icon, size: 18, color: ext.accent),
),
const SizedBox(width: AppSpacing.md),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
link.label,
style: AppTypography.body.copyWith(
fontWeight: FontWeight.w500,
color: ext.textPrimary,
),
),
const SizedBox(height: 2),
Text(
link.url,
style: AppTypography.caption2.copyWith(color: ext.accent),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
GestureDetector(
onTap: () {
Clipboard.setData(ClipboardData(text: link.url));
showCupertinoDialog<void>(
context: context,
builder: (ctx) => CupertinoAlertDialog(
content: Text(t.about.copied),
actions: [
CupertinoDialogAction(
isDefaultAction: true,
onPressed: () => Navigator.pop(ctx),
child: const Text('OK'),
),
],
),
);
},
child: Container(
padding: const EdgeInsets.all(AppSpacing.xs),
decoration: BoxDecoration(
color: ext.accent.withValues(alpha: 0.1),
borderRadius: AppRadius.smBorder,
),
child: Icon(
CupertinoIcons.doc_on_clipboard,
size: 14,
color: ext.accent,
),
),
),
const SizedBox(width: AppSpacing.xs),
Icon(
CupertinoIcons.arrow_up_right_square,
size: 16,
color: ext.accent,
),
],
),
],
),
),
);
}
void _showLaunchDialog(BuildContext context, String url, String label) {
showCupertinoDialog<void>(
context: context,
builder: (ctx) => CupertinoAlertDialog(
title: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
CupertinoIcons.arrow_up_right_square,
color: ext.accent,
size: 18,
),
const SizedBox(width: AppSpacing.sm),
Text(t.about.openLink),
],
),
content: Column(
children: [
const SizedBox(height: AppSpacing.md),
Text(
t.about.leaveAppHint,
style: AppTypography.caption1.copyWith(
color: ext.textSecondary,
height: 1.5,
),
),
const SizedBox(height: AppSpacing.sm),
Text(
label,
style: AppTypography.body.copyWith(
fontWeight: FontWeight.w500,
color: ext.accent,
),
),
const SizedBox(height: AppSpacing.xs),
Text(
url,
style: AppTypography.caption2.copyWith(color: ext.textHint),
),
],
),
actions: [
CupertinoDialogAction(
onPressed: () {
Clipboard.setData(ClipboardData(text: url));
Navigator.of(ctx).pop();
},
child: Text(t.about.copyLink),
),
CupertinoDialogAction(
isDestructiveAction: true,
onPressed: () => Navigator.of(ctx).pop(),
child: Text(t.common.cancel),
),
CupertinoDialogAction(
isDefaultAction: true,
onPressed: () {
Navigator.of(ctx).pop();
launchAboutUrl(context, url);
},
child: Text(t.about.goTo),
),
],
),
);
}
}
// ============================================================
// 通用组件 — 邮箱联系项
// ============================================================
class EmailTile extends StatelessWidget {
const EmailTile({super.key, required this.ext, required this.t});
final AppThemeExtension ext;
final T t;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => _showEmailSheet(context),
behavior: HitTestBehavior.opaque,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.md,
),
child: Row(
children: [
Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: ext.iconTintBlue.withValues(alpha: 0.12),
borderRadius: AppRadius.smBorder,
),
child: Icon(
CupertinoIcons.mail_solid,
size: 18,
color: ext.iconTintBlue,
),
),
const SizedBox(width: AppSpacing.md),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
t.about.contactEmail,
style: AppTypography.body.copyWith(
fontWeight: FontWeight.w500,
color: ext.textPrimary,
),
),
const SizedBox(height: 2),
Text(
t.about.viewEmails,
style: AppTypography.caption1.copyWith(
color: ext.iconTintBlue,
),
),
],
),
),
Icon(
CupertinoIcons.arrow_up_right_square,
size: 16,
color: ext.textHint,
),
],
),
),
);
}
void _showEmailSheet(BuildContext context) {
final emails = [
ContactEmailData(
address: 'contact@s2ss.com',
label: t.about.emailPrimary,
icon: '🌐',
),
ContactEmailData(
address: 'support@s2ss.com',
label: t.about.emailSupport,
icon: '🤝',
),
ContactEmailData(
address: '2821981550@qq.com',
label: t.about.emailAny,
icon: '📬',
),
ContactEmailData(
address: '2572560133@qq.com',
label: t.about.emailAny,
icon: '📬',
),
];
showCupertinoModalPopup<void>(
context: context,
builder: (ctx) => Container(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(ctx).size.height * 0.65,
),
decoration: BoxDecoration(
color: ext.bgElevated,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(AppRadius.xl),
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: AppSpacing.sm),
Container(
width: 36,
height: 4,
decoration: BoxDecoration(
color: ext.textHint.withValues(alpha: 0.3),
borderRadius: AppRadius.smBorder,
),
),
const SizedBox(height: AppSpacing.md),
Padding(
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
child: Row(
children: [
const Text('📧', style: TextStyle(fontSize: 18)),
const SizedBox(width: AppSpacing.sm),
Text(
t.about.contactEmail,
style: AppTypography.title3.copyWith(
fontWeight: FontWeight.w700,
color: ext.textPrimary,
),
),
const Spacer(),
GestureDetector(
onTap: () => Navigator.pop(ctx),
child: Container(
width: 28,
height: 28,
decoration: BoxDecoration(
color: ext.textHint.withValues(alpha: 0.12),
shape: BoxShape.circle,
),
child: Icon(
CupertinoIcons.xmark,
size: 14,
color: ext.textSecondary,
),
),
),
],
),
),
const SizedBox(height: AppSpacing.sm),
Flexible(
child: ListView.separated(
shrinkWrap: true,
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
itemCount: emails.length,
separatorBuilder: (_, __) =>
const SizedBox(height: AppSpacing.sm),
itemBuilder: (_, index) =>
EmailCard(email: emails[index], ext: ext),
),
),
const SizedBox(height: AppSpacing.xl),
],
),
),
);
}
}
class EmailCard extends StatelessWidget {
const EmailCard({super.key, required this.email, required this.ext});
final ContactEmailData email;
final AppThemeExtension ext;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
final uri = Uri(scheme: 'mailto', path: email.address);
ExternalLinkDialog.launchWithConfirm(context, uri: uri);
},
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration(
color: ext.bgPrimary,
borderRadius: AppRadius.lgBorder,
border: Border.all(color: ext.textHint.withValues(alpha: 0.1)),
),
child: Row(
children: [
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: ext.accent.withValues(alpha: 0.08),
borderRadius: AppRadius.mdBorder,
),
child: Center(
child: Text(email.icon, style: const TextStyle(fontSize: 14)),
),
),
const SizedBox(width: AppSpacing.md),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
email.address,
style: AppTypography.body.copyWith(
fontWeight: FontWeight.w500,
color: ext.accent,
),
),
const SizedBox(height: 2),
Text(
email.label,
style: AppTypography.caption2.copyWith(color: ext.textHint),
),
],
),
),
Icon(
CupertinoIcons.arrow_up_right_square,
size: 14,
color: ext.textHint,
),
],
),
),
);
}
}
// ============================================================
// 通用组件 — 微信公众号项
// ============================================================
class WeChatTile extends StatelessWidget {
const WeChatTile({super.key, required this.ext, required this.t});
final AppThemeExtension ext;
final T t;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.md,
),
child: Row(
children: [
Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: const Color(0xFF07C160).withValues(alpha: 0.1),
borderRadius: AppRadius.smBorder,
),
child: const Icon(
CupertinoIcons.chat_bubble_2_fill,
size: 18,
color: Color(0xFF07C160),
),
),
const SizedBox(width: AppSpacing.md),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
t.about.wechatAccount,
style: AppTypography.body.copyWith(
fontWeight: FontWeight.w600,
color: ext.textPrimary,
),
),
const SizedBox(height: 4),
GestureDetector(
onTap: () {
Clipboard.setData(const ClipboardData(text: '微风暴'));
showCupertinoDialog<void>(
context: context,
builder: (ctx) => CupertinoAlertDialog(
content: Text(t.about.copied),
actions: [
CupertinoDialogAction(
isDefaultAction: true,
onPressed: () => Navigator.pop(ctx),
child: const Text('OK'),
),
],
),
);
},
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.sm,
vertical: AppSpacing.xs,
),
decoration: BoxDecoration(
color: const Color(0xFF07C160).withValues(alpha: 0.1),
borderRadius: AppRadius.pillBorder,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
CupertinoIcons.search,
size: 12,
color: Color(0xFF07C160),
),
const SizedBox(width: AppSpacing.xs),
const Text(
'微风暴(微信搜索)',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF07C160),
),
),
const SizedBox(width: AppSpacing.xs),
Icon(
CupertinoIcons.doc_on_clipboard,
size: 12,
color: const Color(0xFF07C160).withValues(alpha: 0.8),
),
],
),
),
),
],
),
),
],
),
);
}
}
// ============================================================
// 通用组件 — 团队成员项
// ============================================================
class TeamMemberTile extends StatelessWidget {
const TeamMemberTile({super.key, required this.member, required this.ext});
final TeamMemberData member;
final AppThemeExtension ext;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.md,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
ext.accentLight.withValues(alpha: 0.6),
ext.accent.withValues(alpha: 0.08),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: AppRadius.mdBorder,
border: Border.all(
color: ext.accent.withValues(alpha: 0.15),
width: 0.5,
),
),
child: Center(
child: Text(member.emoji, style: const TextStyle(fontSize: 20)),
),
),
const SizedBox(width: AppSpacing.md),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Flexible(
child: Text(
member.name,
style: AppTypography.body.copyWith(
fontWeight: FontWeight.w600,
color: ext.textPrimary,
),
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: AppSpacing.xs),
Container(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.xs,
vertical: 2,
),
decoration: BoxDecoration(
color: ext.accent.withValues(alpha: 0.1),
borderRadius: AppRadius.pillBorder,
),
child: Text(
member.role,
style: AppTypography.caption2.copyWith(
color: ext.accent,
),
),
),
],
),
const SizedBox(height: 3),
Text(
member.signature,
style: AppTypography.caption2.copyWith(
color: ext.textHint,
height: 1.4,
),
),
if (member.social.isNotEmpty) ...[
const SizedBox(height: AppSpacing.xs),
Container(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.sm,
vertical: 3,
),
decoration: BoxDecoration(
color: ext.accent.withValues(alpha: 0.06),
borderRadius: AppRadius.pillBorder,
border: Border.all(
color: ext.accent.withValues(alpha: 0.12),
width: 0.5,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
CupertinoIcons.link,
size: 10,
color: ext.accent.withValues(alpha: 0.7),
),
const SizedBox(width: AppSpacing.xs),
Flexible(
child: Text(
member.social,
style: AppTypography.caption2.copyWith(
color: ext.accent.withValues(alpha: 0.85),
),
overflow: TextOverflow.ellipsis,
),
),
],
),
),
],
],
),
),
],
),
);
}
}
// ============================================================
// 工具方法
// ============================================================
Future<void> launchAboutUrl(BuildContext context, String url) async {
final uri = Uri.parse(url);
await ExternalLinkDialog.launchWithConfirm(context, uri: uri);
}
// ============================================================
// 数据类
// ============================================================
class LinkItemData {
const LinkItemData({
required this.icon,
required this.label,
required this.url,
});
final IconData icon;
final String label;
final String url;
}
class TeamMemberData {
const TeamMemberData(
this.emoji,
this.role,
this.name,
this.signature,
this.social,
);
final String emoji;
final String role;
final String name;
final String signature;
final String social;
}
class ContactEmailData {
const ContactEmailData({
required this.address,
required this.label,
required this.icon,
});
final String address;
final String label;
final String icon;
}