10 changed files with 1012 additions and 65 deletions
@ -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<bool> 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<UserCredential> 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<void> 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<void> checkAppleSignInEnvironment() async { |
||||
|
if (kDebugMode) { |
||||
|
print('=== Apple登录环境检查 ==='); |
||||
|
print('平台: ${GetPlatform.isIOS ? "iOS" : "非iOS"}'); |
||||
|
print('Apple登录可用性: ${await SignInWithApple.isAvailable()}'); |
||||
|
print('Firebase Auth实例: ${FirebaseAuth.instance.currentUser?.uid ?? "未登录"}'); |
||||
|
print('========================'); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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, |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -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<MeetingMemberView> createState() => _MeetingMemberViewState(); |
||||
|
} |
||||
|
|
||||
|
class _MeetingMemberViewState extends State<MeetingMemberView> { |
||||
|
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, |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue