From e3a5405f6bbeb95a76228cab03def1a6de9e0ac0 Mon Sep 17 00:00:00 2001 From: tanlongsheng <252620078@qq.com> Date: Thu, 24 Jul 2025 19:24:31 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BC=9A=E5=91=98?= =?UTF-8?q?=E5=BC=B9=E7=AA=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/modules/common/webview_page.dart | 17 +- .../bottomSheet/member_bottom_sheet.dart | 169 ++++++++++++++++++ .../meeting/views/meeting_mine_view.dart | 168 +++++++++-------- .../views/mine/meeting_member_view.dart | 10 ++ 4 files changed, 287 insertions(+), 77 deletions(-) create mode 100644 lib/modules/meeting/views/bottomSheet/member_bottom_sheet.dart create mode 100644 lib/modules/meeting/views/mine/meeting_member_view.dart 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/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..8c58f90aa 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,93 +294,113 @@ 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: EdgeInsets.symmetric( - horizontal: 10.w, - vertical: 6.w, + ], + ), + Padding( + padding: EdgeInsets.only(top: 15.w), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '绑定 Voitrans 设备可激活 Starter 会员', + style: TextStyle( + color: Colors.white, + fontSize: 12.sp, ), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(50.w), - gradient: const LinearGradient( - colors: [Colors.blue, Colors.pink], - begin: Alignment.centerLeft, - end: Alignment.centerRight, + ), + GestureDetector( + onTap: () {}, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: 10.w, + vertical: 6.w, ), - ), - child: Text( - '立即解锁', - style: TextStyle( - color: Colors.white, - fontSize: 14.sp, - fontWeight: FontWeight.bold, + 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, + ), ), ), ), - ), - ], + ], + ), ), - ), - ], + ], + ), ), ), ], 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..44e4328f3 --- /dev/null +++ b/lib/modules/meeting/views/mine/meeting_member_view.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +class MeetingMemberView extends StatelessWidget { + const MeetingMemberView({super.key}); + + @override + Widget build(BuildContext context) { + return const Placeholder(); + } +} From 4fb54e59a167171fad73fb4b8958470706153c56 Mon Sep 17 00:00:00 2001 From: tanlongsheng <252620078@qq.com> Date: Thu, 24 Jul 2025 22:15:15 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BC=9A=E5=91=98?= =?UTF-8?q?=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../meeting/views/meeting_mine_view.dart | 39 +- .../views/mine/meeting_member_view.dart | 531 +++++++++++++++++- 2 files changed, 547 insertions(+), 23 deletions(-) diff --git a/lib/modules/meeting/views/meeting_mine_view.dart b/lib/modules/meeting/views/meeting_mine_view.dart index 8c58f90aa..c2aa57307 100644 --- a/lib/modules/meeting/views/meeting_mine_view.dart +++ b/lib/modules/meeting/views/meeting_mine_view.dart @@ -371,28 +371,25 @@ class _MeetingMineViewState extends State fontSize: 12.sp, ), ), - GestureDetector( - onTap: () {}, - child: 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, - ), + 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, - ), + ), + child: Text( + '立即解锁', + style: TextStyle( + color: Colors.white, + fontSize: 14.sp, + fontWeight: FontWeight.bold, ), ), ), diff --git a/lib/modules/meeting/views/mine/meeting_member_view.dart b/lib/modules/meeting/views/mine/meeting_member_view.dart index 44e4328f3..b5caf7563 100644 --- a/lib/modules/meeting/views/mine/meeting_member_view.dart +++ b/lib/modules/meeting/views/mine/meeting_member_view.dart @@ -1,10 +1,537 @@ import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; -class MeetingMemberView extends StatelessWidget { +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 const Placeholder(); + 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, + ), + ); } } From e8841d917c1a9914e34e4314c8471d4f6d638901 Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Fri, 25 Jul 2025 10:44:49 +0800 Subject: [PATCH 3/3] =?UTF-8?q?feat(=E7=99=BB=E5=BD=95):=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0Apple=E7=99=BB=E5=BD=95=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加sign_in_with_apple依赖 - 配置iOS entitlements和Info.plist - 实现Apple登录按钮及控制器逻辑 - 新增Apple登录处理类 - 根据地区显示不同登录选项 --- ios/Runner/Info.plist | 8 + ios/Runner/Runner.entitlements | 4 + lib/modules/login/controllers/appleLogin.dart | 158 ++++++++++++++++++ .../login/controllers/login_controller.dart | 32 ++++ lib/modules/login/views/login_view.dart | 18 +- pubspec.yaml | 1 + 6 files changed, 217 insertions(+), 4 deletions(-) create mode 100644 lib/modules/login/controllers/appleLogin.dart 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/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/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