diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index 40323d9d6..ca4a8bf61 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -38,6 +38,14 @@
wx5bbb7be9324f3e2d
+
+ CFBundleURLName
+ com.saitong.voitrans.signin
+ CFBundleURLSchemes
+
+ com.saitong.voitrans
+
+
CFBundleVersion
$(FLUTTER_BUILD_NUMBER)
diff --git a/ios/Runner/Runner.entitlements b/ios/Runner/Runner.entitlements
index 0286ef500..88b018abb 100644
--- a/ios/Runner/Runner.entitlements
+++ b/ios/Runner/Runner.entitlements
@@ -12,5 +12,9 @@
com.apple.external-accessory.wireless-configuration
+ com.apple.developer.applesignin
+
+ Default
+
diff --git a/lib/modules/common/webview_page.dart b/lib/modules/common/webview_page.dart
index b80bf348b..7056077f2 100644
--- a/lib/modules/common/webview_page.dart
+++ b/lib/modules/common/webview_page.dart
@@ -20,8 +20,9 @@ class WebViewPage extends StatelessWidget {
return Scaffold(
backgroundColor: isDarkMode ? Colors.black : Colors.grey[50],
appBar: AppBar(
- centerTitle: true,
- title: Text(title),
+ backgroundColor: isDarkMode ? Colors.black : Colors.white,
+ surfaceTintColor: Colors.transparent,
+ elevation: 0.5,
leading: IconButton(
icon: Icon(
Icons.arrow_back_ios,
@@ -30,9 +31,15 @@ class WebViewPage extends StatelessWidget {
),
onPressed: () => Get.back(),
),
- backgroundColor: isDarkMode ? Colors.black : Colors.white,
- foregroundColor: isDarkMode ? Colors.white : Colors.black,
- elevation: 0,
+ title: Text(
+ title,
+ style: TextStyle(
+ fontSize: 17.sp,
+ fontWeight: FontWeight.w600,
+ color: isDarkMode ? Colors.white : Colors.black,
+ ),
+ ),
+ centerTitle: true,
),
body: WebViewWidget(
controller: controller,
diff --git a/lib/modules/login/controllers/appleLogin.dart b/lib/modules/login/controllers/appleLogin.dart
new file mode 100644
index 000000000..f77b86393
--- /dev/null
+++ b/lib/modules/login/controllers/appleLogin.dart
@@ -0,0 +1,158 @@
+import 'dart:convert';
+import 'dart:io';
+import 'dart:math';
+import 'package:firebase_auth/firebase_auth.dart';
+import 'package:flutter/foundation.dart';
+import 'package:sign_in_with_apple/sign_in_with_apple.dart';
+import 'package:crypto/crypto.dart';
+import 'package:get/get.dart';
+
+class Apple {
+ String? token = '';
+ String name = '';
+ String email = '';
+ String pictureurl = '';
+ String id = '';
+
+ /// 生成随机nonce字符串
+ String generateNonce([int length = 32]) {
+ const charset =
+ '0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._';
+ final random = Random.secure();
+ return List.generate(length, (_) => charset[random.nextInt(charset.length)])
+ .join();
+ }
+
+ /// 返回输入字符串的sha256哈希值(十六进制表示)
+ String sha256ofString(String input) {
+ final bytes = utf8.encode(input);
+ final digest = sha256.convert(bytes);
+ return digest.toString();
+ }
+
+ /// Apple登录主方法
+ Future signInWithApple() async {
+ try {
+ // 检查Apple登录可用性
+ if (!await SignInWithApple.isAvailable()) {
+ if (kDebugMode) {
+ print('Apple登录不可用');
+ }
+ return false;
+ }
+
+ UserCredential userCredential = await loginApple();
+ token = await userCredential.user?.getIdToken();
+
+ if (kDebugMode) {
+ print('Apple登录响应数据token: $token');
+ }
+
+ return token != null && token!.isNotEmpty;
+ } catch (e) {
+ if (kDebugMode) {
+ print('Apple登录异常:$e');
+ }
+ return false;
+ }
+ }
+
+ /// 执行Apple登录流程
+ Future loginApple() async {
+ // 生成nonce用于安全验证
+ final rawNonce = generateNonce();
+ final nonce = sha256ofString(rawNonce);
+
+ try {
+ // 请求Apple登录凭证
+ final appleCredential = await SignInWithApple.getAppleIDCredential(
+ scopes: [
+ AppleIDAuthorizationScopes.email,
+ AppleIDAuthorizationScopes.fullName,
+ ],
+ nonce: nonce,
+ );
+
+ // 提取用户信息
+ _extractUserInfo(appleCredential);
+
+ // 创建Firebase OAuth凭证
+ final oauthCredential = OAuthProvider("apple.com").credential(
+ idToken: appleCredential.identityToken,
+ rawNonce: rawNonce,
+ );
+
+ if (kDebugMode) {
+ print('Apple OAuth凭证创建成功');
+ print('用户信息: name=$name, email=$email, id=$id');
+ }
+
+ // 使用Firebase Auth进行登录
+ return await FirebaseAuth.instance.signInWithCredential(oauthCredential);
+ } catch (e) {
+ if (kDebugMode) {
+ print('Apple登录流程异常:$e');
+ }
+ throw Exception('Apple登录失败: $e');
+ }
+ }
+
+ /// 提取Apple返回的用户信息
+ void _extractUserInfo(AuthorizationCredentialAppleID credential) {
+ // 用户ID
+ id = credential.userIdentifier ?? '';
+
+ // 邮箱(可能为空,如果用户选择隐藏邮箱)
+ email = credential.email ?? '';
+
+ // 用户姓名(仅首次登录时提供)
+ if (credential.givenName != null || credential.familyName != null) {
+ final givenName = credential.givenName ?? '';
+ final familyName = credential.familyName ?? '';
+ name = '$givenName $familyName'.trim();
+ }
+
+ // Apple不提供头像,使用默认值
+ pictureurl = '';
+
+ if (kDebugMode) {
+ print('提取的用户信息:');
+ print('ID: $id');
+ print('Email: $email');
+ print('Name: $name');
+ print('Picture URL: $pictureurl');
+ }
+ }
+
+ /// 退出Apple登录
+ Future signOutApple() async {
+ try {
+ await FirebaseAuth.instance.signOut();
+ // 清空用户信息
+ token = '';
+ name = '';
+ email = '';
+ pictureurl = '';
+ id = '';
+
+ if (kDebugMode) {
+ print('Apple登录退出成功');
+ }
+ } catch (e) {
+ if (kDebugMode) {
+ print('Apple登录退出异常:$e');
+ }
+ }
+ }
+
+ /// 检查Apple登录环境
+ static Future checkAppleSignInEnvironment() async {
+ if (kDebugMode) {
+ print('=== Apple登录环境检查 ===');
+ print('平台: ${GetPlatform.isIOS ? "iOS" : "非iOS"}');
+ print('Apple登录可用性: ${await SignInWithApple.isAvailable()}');
+ print('Firebase Auth实例: ${FirebaseAuth.instance.currentUser?.uid ?? "未登录"}');
+ print('========================');
+ }
+ }
+}
diff --git a/lib/modules/login/controllers/login_controller.dart b/lib/modules/login/controllers/login_controller.dart
index c77588c20..6852634ef 100644
--- a/lib/modules/login/controllers/login_controller.dart
+++ b/lib/modules/login/controllers/login_controller.dart
@@ -8,6 +8,7 @@ import 'dart:convert';
import 'facebookLogin.dart';
import 'googleLogin.dart';
import 'weixinLogin.dart';
+import 'appleLogin.dart';
import '../../../data/services/network/api.dart';
import '../../../../data/models/user_Info.dart';
import '../../common/webview_page.dart';
@@ -50,6 +51,7 @@ class LoginController extends GetxController {
String _appVersion = ''; // APP版本号
String _countryCode = ''; // 国家代码
UserChannel _userChannel = UserChannel.OFFICIAL; // 默认渠道
+ final isChinaMainland = false.obs; //是否中国大陆地区
final _hasAcceptedPrivacy = false.obs;
bool get hasAcceptedPrivacy => _hasAcceptedPrivacy.value;
@@ -202,6 +204,9 @@ class LoginController extends GetxController {
super.onInit();
_viewState.value = LoginViewState.welcome;
+ isChinaMainland.value =
+ Get.deviceLocale?.countryCode?.toUpperCase() == 'CN';
+
_hasAcceptedPrivacy.value =
_storage.read('hasAcceptedPrivacy') ?? false; // 从本地存储中读取是否已接受隐私政策
@@ -874,6 +879,33 @@ class LoginController extends GetxController {
}
break;
}
+ case 5: //Apple登录
+ {
+ Logger().d("数据获取Apple");
+ final apple = Apple();
+ bool authResult = await apple.signInWithApple();
+
+ if (authResult) {
+ Logger().d("数据获取_Apple.token${apple.token}");
+ Logger().d("数据获取_Apple.email${apple.email}");
+ Logger().d("数据获取_Apple.name${apple.name}");
+ Logger().d("数据获取_Apple.id${apple.id}");
+
+ // 验证成功,跳转到配对页面
+ requestLogin({
+ "mail": apple.email,
+ "phone": "",
+ "stype": loginType,
+ "vcode": "",
+ "ttoken": apple.token,
+ "phonemodel": "",
+ "phonemac": _deviceId,
+ "name": apple.name,
+ "avatar": apple.pictureurl,
+ });
+ }
+ break;
+ }
case 6: //游客登录
{
requestLogin({
diff --git a/lib/modules/login/views/login_view.dart b/lib/modules/login/views/login_view.dart
index 22fbc93b3..bd992b5a9 100644
--- a/lib/modules/login/views/login_view.dart
+++ b/lib/modules/login/views/login_view.dart
@@ -481,13 +481,18 @@ class LoginView extends GetView {
SizedBox(width: 24.w),
_buildSocialButton(
FontAwesomeIcons.weixin, Colors.green, isDarkMode),
- SizedBox(width: 24.w),
- _buildSocialButton(
- FontAwesomeIcons.facebook, Colors.blue, isDarkMode),
+ // Apple登录按钮(仅在iOS上显示)
+ if (GetPlatform.isIOS) ...[
+ SizedBox(width: 24.w),
+ _buildSocialButton(FontAwesomeIcons.apple, Colors.black, isDarkMode),
+ ],
+ // SizedBox(width: 24.w),
+ // _buildSocialButton(
+ // FontAwesomeIcons.facebook, Colors.blue, isDarkMode),
// 根据配置显示游客登录按钮
if (controller.isGuestLoginVisible) ...[
SizedBox(width: 24.w),
- _buildSocialButton(FontAwesomeIcons.userSecret, Colors.black,
+ _buildSocialButton(FontAwesomeIcons.userSecret, Colors.grey,
isDarkMode), // 游客登录按钮
],
],
@@ -541,6 +546,11 @@ class LoginView extends GetView {
controller.loginType = 2;
break;
}
+ case FontAwesomeIcons.apple: // 处理Apple登录
+ {
+ controller.loginType = 5;
+ break;
+ }
case FontAwesomeIcons.userSecret: // 处理游客登录
{
controller.loginType = 6;
diff --git a/lib/modules/meeting/views/bottomSheet/member_bottom_sheet.dart b/lib/modules/meeting/views/bottomSheet/member_bottom_sheet.dart
new file mode 100644
index 000000000..e77769091
--- /dev/null
+++ b/lib/modules/meeting/views/bottomSheet/member_bottom_sheet.dart
@@ -0,0 +1,169 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:get/get.dart';
+
+import '../../../common/webview_page.dart';
+
+class MemberBottomSheet extends StatelessWidget {
+ const MemberBottomSheet({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ padding: EdgeInsets.symmetric(horizontal: 12.w),
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.vertical(
+ top: Radius.circular(12.r),
+ ),
+ image: const DecorationImage(
+ alignment: Alignment.topCenter,
+ image: AssetImage('assets/images/mine-background.png'),
+ fit: BoxFit.fitWidth,
+ ),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Container(
+ margin: EdgeInsets.only(top: 12.w),
+ alignment: Alignment.centerRight,
+ child: GestureDetector(
+ onTap: () => Get.back(),
+ child: Container(
+ width: 26.w,
+ height: 26.w,
+ decoration: BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.circular(26.r),
+ ),
+ child: Icon(
+ Icons.close,
+ size: 18.w,
+ color: Colors.grey[400],
+ ),
+ ),
+ ),
+ ),
+ Text(
+ '解锁 Starter 会员',
+ style: TextStyle(
+ color: Colors.black87,
+ fontSize: 18.sp,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ Padding(
+ padding: EdgeInsets.only(top: 5.w, bottom: 20.w),
+ child: Text(
+ '绑定 VOITRANS 设备可激活 Starter 会员权益,解锁:',
+ style: TextStyle(
+ color: Colors.grey,
+ fontSize: 14.sp,
+ ),
+ ),
+ ),
+ _item(Icons.access_time, Colors.red, '每月300分钟转写时长'),
+ _item(Icons.description, Colors.purpleAccent, '为行业细分场景设计的专业总结模版'),
+ _item(Icons.auto_awesome, Colors.purpleAccent, 'Autopilot 模版自动总结'),
+ _item(Icons.people, Colors.red, '区分不同说话人'),
+ _item(Icons.cloud_upload, Colors.red, '从APP、WEB导入音频进行转写和总结'),
+ Padding(
+ padding: EdgeInsets.symmetric(vertical: 20.w),
+ child: Row(
+ children: [
+ Expanded(
+ child: GestureDetector(
+ onTap: () {
+ Get.to(
+ () => const WebViewPage(
+ url: 'https://web.voitrans.net',
+ title: 'Voitrans',
+ ),
+ );
+ },
+ child: Container(
+ height: 48.w,
+ alignment: Alignment.center,
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(12.r),
+ border: Border.all(
+ color: Colors.grey[400]!,
+ ),
+ ),
+ child: Text(
+ '发现更多',
+ style: TextStyle(
+ fontSize: 16.sp,
+ color: Colors.black,
+ ),
+ ),
+ ),
+ ),
+ ),
+ 10.horizontalSpace,
+ Expanded(
+ child: GestureDetector(
+ onTap: () {},
+ child: Container(
+ height: 48.w,
+ alignment: Alignment.center,
+ decoration: BoxDecoration(
+ color: Colors.black,
+ borderRadius: BorderRadius.circular(12.r),
+ ),
+ child: Text(
+ '立即绑定',
+ style: TextStyle(
+ fontSize: 16.sp,
+ color: Colors.white,
+ ),
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _item(IconData icon, Color color, String title) {
+ return Padding(
+ padding: EdgeInsets.symmetric(vertical: 10.w),
+ child: Row(
+ children: [
+ Container(
+ width: 50.w,
+ height: 50.w,
+ margin: EdgeInsets.only(right: 10.w),
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(16.r),
+ border: Border.all(
+ color: Colors.red.withOpacity(0.1),
+ width: 2.w,
+ ),
+ ),
+ child: Icon(
+ icon,
+ size: 24.w,
+ color: color,
+ ),
+ ),
+ Expanded(
+ child: Text(
+ title,
+ style: TextStyle(
+ color: Colors.black87,
+ fontSize: 14.sp,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/modules/meeting/views/meeting_mine_view.dart b/lib/modules/meeting/views/meeting_mine_view.dart
index b3609f031..c2aa57307 100644
--- a/lib/modules/meeting/views/meeting_mine_view.dart
+++ b/lib/modules/meeting/views/meeting_mine_view.dart
@@ -7,8 +7,10 @@ import '../../profile/views/unified_avatar.dart';
import '../controllers/meeting_controller.dart';
import '../controllers/meeting_mine_controller.dart';
import '../utils/meeting_ui_utils.dart';
+import 'bottomSheet/member_bottom_sheet.dart';
import 'bottomSheet/statistics_bottom_sheet.dart';
import 'mine/meeting_cloud_view.dart';
+import 'mine/meeting_member_view.dart';
import 'mine/meeting_transcription_view.dart';
class MeetingMineView extends StatefulWidget {
@@ -96,7 +98,9 @@ class _MeetingMineViewState extends State
_item(
Icons.settings_remote_outlined,
'我的VOITRANS设备',
- onTap: () {},
+ onTap: () {
+ Get.toNamed(Routes.devices);
+ },
),
]),
_box([
@@ -290,67 +294,84 @@ class _MeetingMineViewState extends State
),
child: Column(
children: [
- Padding(
- padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 10.w),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- Text(
- 'Voitrans AI会员',
- style: TextStyle(
- color: MeetingUIUtils.getTextColor(isDarkMode),
- fontSize: 16.sp,
+ GestureDetector(
+ onTap: () {
+ Get.to(() => const MeetingMemberView());
+ },
+ behavior: HitTestBehavior.translucent,
+ child: Padding(
+ padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 10.w),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(
+ 'Voitrans AI会员',
+ style: TextStyle(
+ color: MeetingUIUtils.getTextColor(isDarkMode),
+ fontSize: 16.sp,
+ ),
),
- ),
- Icon(
- Icons.arrow_forward_ios,
- size: 16.w,
- color: Colors.grey,
- ),
- ],
+ Icon(
+ Icons.arrow_forward_ios,
+ size: 16.w,
+ color: Colors.grey,
+ ),
+ ],
+ ),
),
),
- Container(
- padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 15.w),
- decoration: BoxDecoration(
- color: Colors.black87,
- borderRadius: BorderRadius.circular(16.w),
- ),
- child: Column(
- children: [
- Row(
- children: [
- Icon(
- Icons.auto_awesome,
- color: Colors.amberAccent,
- size: 25.sp,
- ),
- 5.horizontalSpace,
- Text(
- '解锁 Starter 会员',
- style: TextStyle(
- color: Colors.white,
- fontSize: 18.sp,
- fontWeight: FontWeight.bold,
- ),
- ),
- ],
+ GestureDetector(
+ onTap: () {
+ Get.bottomSheet(
+ const MemberBottomSheet(),
+ isScrollControlled: true,
+ backgroundColor: Colors.white,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.vertical(
+ top: Radius.circular(12.r),
+ ),
),
- Padding(
- padding: EdgeInsets.only(top: 15.w),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ );
+ },
+ child: Container(
+ padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 15.w),
+ decoration: BoxDecoration(
+ color: Colors.black87,
+ borderRadius: BorderRadius.circular(16.w),
+ ),
+ child: Column(
+ children: [
+ Row(
children: [
+ Icon(
+ Icons.auto_awesome,
+ color: Colors.amberAccent,
+ size: 25.sp,
+ ),
+ 5.horizontalSpace,
Text(
- '绑定 Voitrans 设备可激活 Starter 会员',
+ '解锁 Starter 会员',
style: TextStyle(
color: Colors.white,
- fontSize: 12.sp,
+ fontSize: 18.sp,
+ fontWeight: FontWeight.bold,
),
),
- GestureDetector(
- onTap: () {},
- child: Container(
+ ],
+ ),
+ Padding(
+ padding: EdgeInsets.only(top: 15.w),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(
+ '绑定 Voitrans 设备可激活 Starter 会员',
+ style: TextStyle(
+ color: Colors.white,
+ fontSize: 12.sp,
+ ),
+ ),
+ Container(
padding: EdgeInsets.symmetric(
horizontal: 10.w,
vertical: 6.w,
@@ -372,11 +393,11 @@ class _MeetingMineViewState extends State
),
),
),
- ),
- ],
+ ],
+ ),
),
- ),
- ],
+ ],
+ ),
),
),
],
diff --git a/lib/modules/meeting/views/mine/meeting_member_view.dart b/lib/modules/meeting/views/mine/meeting_member_view.dart
new file mode 100644
index 000000000..b5caf7563
--- /dev/null
+++ b/lib/modules/meeting/views/mine/meeting_member_view.dart
@@ -0,0 +1,537 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:get/get.dart';
+
+import '../../../profile/views/unified_avatar.dart';
+import '../../controllers/meeting_mine_controller.dart';
+import '../bottomSheet/member_bottom_sheet.dart';
+
+class MeetingMemberView extends StatefulWidget {
+ const MeetingMemberView({super.key});
+
+ @override
+ State createState() => _MeetingMemberViewState();
+}
+
+class _MeetingMemberViewState extends State {
+ final _controller = Get.put(MeetingMineController());
+
+ int _tabsIndex = 0;
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ backgroundColor: Colors.black,
+ appBar: _buildAppBar(),
+ body: _buildBody(),
+ );
+ }
+
+ // 构建应用栏
+ PreferredSizeWidget _buildAppBar() {
+ return AppBar(
+ backgroundColor: Colors.black,
+ surfaceTintColor: Colors.transparent,
+ elevation: 0.5,
+ leading: IconButton(
+ icon: Icon(
+ Icons.arrow_back_ios,
+ color: Colors.white,
+ size: 20.sp,
+ ),
+ onPressed: () => Get.back(),
+ ),
+ title: Text(
+ 'VOITRANS AI会员',
+ style: TextStyle(
+ fontSize: 17.sp,
+ fontWeight: FontWeight.w600,
+ color: Colors.white,
+ ),
+ ),
+ centerTitle: true,
+ );
+ }
+
+ Widget _buildBody() {
+ return Container(
+ padding: EdgeInsets.symmetric(horizontal: 12.w),
+ child: ListView(
+ children: [
+ _head(),
+ _member(),
+ _tabs(),
+ _comparison(),
+ ],
+ ),
+ );
+ }
+
+ Widget _head() {
+ return Padding(
+ padding: EdgeInsets.symmetric(vertical: 20.w),
+ child: Row(
+ children: [
+ Container(
+ margin: EdgeInsets.only(right: 10.w),
+ child: UnifiedAvatar(
+ radius: 25.w,
+ backgroundColor: Colors.grey[200],
+ iconColor: Colors.grey[600],
+ iconSize: 25.r,
+ isDarkMode: true,
+ ),
+ ),
+ Expanded(
+ child: Obx(
+ () => Text(
+ _controller.profileCtrl.userInfo.value.name,
+ style: TextStyle(
+ color: Colors.white,
+ fontSize: 20.sp,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _member() {
+ return GestureDetector(
+ onTap: () {
+ Get.bottomSheet(
+ const MemberBottomSheet(),
+ isScrollControlled: true,
+ backgroundColor: Colors.white,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.vertical(
+ top: Radius.circular(12.r),
+ ),
+ ),
+ );
+ },
+ child: Container(
+ padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 15.w),
+ decoration: BoxDecoration(
+ color: const Color(0xFF1A1A1A),
+ borderRadius: BorderRadius.circular(16.w),
+ ),
+ child: Column(
+ children: [
+ Row(
+ children: [
+ Icon(
+ Icons.auto_awesome,
+ color: Colors.amberAccent,
+ size: 25.sp,
+ ),
+ 5.horizontalSpace,
+ Text(
+ '解锁 Starter 会员',
+ style: TextStyle(
+ color: Colors.white,
+ fontSize: 18.sp,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ Padding(
+ padding: EdgeInsets.only(top: 15.w),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(
+ '绑定 Voitrans 设备可激活 Starter 会员',
+ style: TextStyle(
+ color: Colors.white,
+ fontSize: 12.sp,
+ ),
+ ),
+ Container(
+ padding: EdgeInsets.symmetric(
+ horizontal: 10.w,
+ vertical: 6.w,
+ ),
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(50.w),
+ gradient: const LinearGradient(
+ colors: [Colors.blue, Colors.pink],
+ begin: Alignment.centerLeft,
+ end: Alignment.centerRight,
+ ),
+ ),
+ child: Text(
+ '立即解锁',
+ style: TextStyle(
+ color: Colors.white,
+ fontSize: 14.sp,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _tabs() {
+ return Container(
+ padding: EdgeInsets.all(2.w),
+ margin: EdgeInsets.only(top: 20.w, bottom: 5.w),
+ decoration: BoxDecoration(
+ color: Colors.grey[700],
+ borderRadius: BorderRadius.circular(10.w),
+ ),
+ child: Row(
+ children: [
+ Expanded(
+ child: GestureDetector(
+ onTap: () {
+ setState(() {
+ _tabsIndex = 0;
+ });
+ },
+ behavior: HitTestBehavior.translucent,
+ child: Container(
+ height: 30.w,
+ alignment: Alignment.center,
+ decoration: _tabsIndex == 0
+ ? BoxDecoration(
+ color: Colors.grey[50],
+ borderRadius: BorderRadius.circular(8.w),
+ )
+ : null,
+ child: Text(
+ '对比',
+ style: TextStyle(
+ color: _tabsIndex == 0 ? Colors.black : Colors.white,
+ fontSize: 14.sp,
+ fontWeight: _tabsIndex == 0 ? FontWeight.bold : null,
+ ),
+ ),
+ ),
+ ),
+ ),
+ Expanded(
+ child: GestureDetector(
+ onTap: () {
+ setState(() {
+ _tabsIndex = 1;
+ });
+ },
+ behavior: HitTestBehavior.translucent,
+ child: Container(
+ height: 30.w,
+ alignment: Alignment.center,
+ decoration: _tabsIndex == 1
+ ? BoxDecoration(
+ color: Colors.grey[50],
+ borderRadius: BorderRadius.circular(8.w),
+ )
+ : null,
+ child: Text(
+ '详细信息',
+ style: TextStyle(
+ color: _tabsIndex == 1 ? Colors.black : Colors.white,
+ fontSize: 14.sp,
+ fontWeight: _tabsIndex == 1 ? FontWeight.bold : null,
+ ),
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _comparison() {
+ return Column(
+ children: [
+ Container(
+ height: 50.w,
+ decoration: BoxDecoration(
+ border: Border(
+ bottom: BorderSide(
+ color: Colors.grey[700]!,
+ width: 0.5,
+ ),
+ ),
+ ),
+ child: Row(
+ children: [
+ const Expanded(child: SizedBox.square()),
+ Container(
+ width: 60.w,
+ alignment: Alignment.center,
+ child: Text(
+ 'Starter',
+ style: TextStyle(
+ color: Colors.grey[200],
+ fontSize: 14.sp,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ),
+ Container(
+ width: 70.w,
+ alignment: Alignment.center,
+ child: Container(
+ width: 40.w,
+ height: 24.w,
+ alignment: Alignment.center,
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(8.w),
+ gradient: const LinearGradient(
+ colors: [Colors.blue, Colors.pink],
+ begin: Alignment.centerLeft,
+ end: Alignment.centerRight,
+ ),
+ ),
+ child: Text(
+ 'Pro',
+ style: TextStyle(
+ color: Colors.white,
+ fontSize: 14.sp,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ),
+ ),
+ Container(
+ width: 72.w,
+ alignment: Alignment.center,
+ child: Container(
+ width: 72.w,
+ height: 24.w,
+ alignment: Alignment.center,
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(8.w),
+ gradient: const LinearGradient(
+ colors: [
+ Colors.amberAccent,
+ Colors.white,
+ Colors.amberAccent
+ ],
+ begin: Alignment.centerLeft,
+ end: Alignment.centerRight,
+ ),
+ ),
+ child: Text(
+ 'Unlimited',
+ style: TextStyle(
+ color: Colors.black,
+ fontSize: 14.sp,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ _tables(
+ '每月转写时长',
+ '300',
+ '1200',
+ '无限',
+ isGradient: true,
+ ),
+ _tables(
+ '专业模版',
+ '有限',
+ '全量模版',
+ '全量模版',
+ ),
+ _tables(
+ '专业术语',
+ '—',
+ '',
+ '',
+ ),
+ _tables(
+ '自定义模版',
+ '—',
+ '',
+ '',
+ ),
+ _tables(
+ 'Ask AI',
+ '—',
+ '',
+ '',
+ ),
+ _tables(
+ '可视化思维导图',
+ '',
+ '',
+ '',
+ ),
+ _tables(
+ '区分说话人',
+ '',
+ '',
+ '',
+ ),
+ _tables(
+ 'Private Cloud Sync',
+ '',
+ '',
+ '',
+ ),
+ _tables(
+ 'AI 人声增强',
+ '',
+ '',
+ '',
+ ),
+ _tables(
+ '音频导入',
+ '',
+ '',
+ '',
+ ),
+ _tables(
+ '支持多种导出格式',
+ '',
+ '',
+ '',
+ ),
+ ],
+ );
+ }
+
+ Widget _tables(
+ String title,
+ String starter,
+ String pro,
+ String unlimited, {
+ bool isGradient = false,
+ }) {
+ return Container(
+ height: 50.w,
+ decoration: BoxDecoration(
+ border: Border(
+ bottom: BorderSide(
+ color: Colors.grey[700]!,
+ width: 0.5,
+ ),
+ ),
+ ),
+ child: Row(
+ children: [
+ Expanded(
+ child: Text(
+ title,
+ style: TextStyle(
+ color: Colors.grey[200],
+ fontSize: 12.sp,
+ ),
+ ),
+ ),
+ Container(
+ width: 60.w,
+ alignment: Alignment.center,
+ child: starter.isNotEmpty
+ ? Text(
+ starter,
+ style: TextStyle(
+ color: Colors.grey[200],
+ fontSize: 12.sp,
+ fontWeight: FontWeight.bold,
+ ),
+ )
+ : _check(false),
+ ),
+ Container(
+ width: 70.w,
+ alignment: Alignment.center,
+ child: pro.isNotEmpty
+ ? isGradient
+ ? ShaderMask(
+ shaderCallback: (bounds) => const LinearGradient(
+ colors: [Colors.blue, Colors.pink],
+ begin: Alignment.centerLeft,
+ end: Alignment.centerRight,
+ ).createShader(bounds),
+ child: Text(
+ pro,
+ style: TextStyle(
+ color: Colors.white,
+ fontSize: 12.sp,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ )
+ : Text(
+ pro,
+ style: TextStyle(
+ color: Colors.white,
+ fontSize: 12.sp,
+ fontWeight: FontWeight.bold,
+ ),
+ )
+ : _check(isGradient),
+ ),
+ Container(
+ width: 72.w,
+ alignment: Alignment.center,
+ child: unlimited.isNotEmpty
+ ? ShaderMask(
+ shaderCallback: (bounds) => const LinearGradient(
+ colors: [
+ Colors.amberAccent,
+ Colors.yellowAccent,
+ Colors.amberAccent,
+ ],
+ begin: Alignment.centerLeft,
+ end: Alignment.centerRight,
+ ).createShader(bounds),
+ child: Text(
+ unlimited,
+ style: TextStyle(
+ color: Colors.white,
+ fontSize: 12.sp,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ )
+ : _check(true),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _check(bool isGradient) {
+ return Container(
+ width: 20.w,
+ height: 20.w,
+ alignment: Alignment.center,
+ decoration: BoxDecoration(
+ color: Colors.grey[400],
+ borderRadius: BorderRadius.circular(20.w),
+ gradient: isGradient
+ ? const LinearGradient(
+ colors: [Colors.amberAccent, Colors.white, Colors.amberAccent],
+ begin: Alignment.centerLeft,
+ end: Alignment.centerRight,
+ )
+ : null,
+ ),
+ child: Icon(
+ Icons.check,
+ color: Colors.black,
+ size: 16.sp,
+ ),
+ );
+ }
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index 1abb24202..c601eebd1 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -65,6 +65,7 @@ dependencies:
firebase_auth: ^5.5.1
google_sign_in: ^6.2.2
flutter_facebook_auth: ^7.1.1
+ sign_in_with_apple: ^6.1.2
flutter_image_compress: ^2.4.0
common_utils: ^2.1.0
image_gallery_saver_plus: ^4.0.1