|
|
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; |
|
|
|
import 'package:get/get.dart'; |
|
|
|
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
|
|
|
import 'package:voitrans/data/models/goods.dart'; |
|
|
|
import '../../../data/models/user_Info.dart'; |
|
|
|
import '../controllers/goodsvip_controller.dart'; |
|
|
|
import '../utils/goods_i18n.dart'; |
|
|
|
|
|
|
|
@ -15,14 +16,19 @@ class GoodsvipView extends GetView<GoodsvipController> { |
|
|
|
return Scaffold( |
|
|
|
backgroundColor: isDarkMode ? Colors.black : Colors.grey[50], |
|
|
|
appBar: AppBar( |
|
|
|
backgroundColor: isDarkMode ? Colors.black : Colors.white, |
|
|
|
backgroundColor: Colors.blue[600], |
|
|
|
foregroundColor: Colors.white, |
|
|
|
elevation: 0.5, |
|
|
|
leading: IconButton( |
|
|
|
icon: const Icon(Icons.arrow_back_ios_new), |
|
|
|
onPressed: () => Get.back(), |
|
|
|
), |
|
|
|
title: Text( |
|
|
|
'premiumMember'.tr, // 高级会员 |
|
|
|
'购买', |
|
|
|
style: TextStyle( |
|
|
|
fontSize: 18.sp, |
|
|
|
fontWeight: FontWeight.w600, |
|
|
|
color: isDarkMode ? Colors.white : Colors.black, |
|
|
|
color: Colors.white, |
|
|
|
), |
|
|
|
), |
|
|
|
centerTitle: true, |
|
|
|
@ -34,20 +40,38 @@ class GoodsvipView extends GetView<GoodsvipController> { |
|
|
|
} else if (controller.error.isNotEmpty) { |
|
|
|
content = Center(child: Text(controller.error.value)); |
|
|
|
} else { |
|
|
|
content = ListView.separated( |
|
|
|
final Map<UsageType, List<DBGoods>> grouped = {}; |
|
|
|
for (final g in controller.items) { |
|
|
|
grouped.putIfAbsent(g.usagetype, () => []).add(g); |
|
|
|
} |
|
|
|
final order = [ |
|
|
|
UsageType.Vip, |
|
|
|
UsageType.Translation, |
|
|
|
UsageType.Meet, |
|
|
|
UsageType.AI, |
|
|
|
]; |
|
|
|
final children = <Widget>[]; |
|
|
|
children.add(_buildFeaturesSection(isDarkMode)); |
|
|
|
children.add(SizedBox(height: 16.h)); |
|
|
|
// children.add(_buildScenesSection(isDarkMode)); |
|
|
|
// children.add(SizedBox(height: 16.h)); |
|
|
|
for (int i = 0; i < order.length; i++) { |
|
|
|
final t = order[i]; |
|
|
|
final list = grouped[t]; |
|
|
|
if (list == null || list.isEmpty) continue; |
|
|
|
children.add(_buildTypeSection(t, list, isDarkMode)); |
|
|
|
if (i != order.length - 1) { |
|
|
|
children.add(SizedBox(height: 16.h)); |
|
|
|
children.add( |
|
|
|
Divider(color: isDarkMode ? Colors.white24 : Colors.black12)); |
|
|
|
children.add(SizedBox(height: 16.h)); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
children.add(_buildFooterSection(isDarkMode)); |
|
|
|
content = ListView( |
|
|
|
padding: EdgeInsets.all(16.w), |
|
|
|
itemCount: controller.items.length + 2, |
|
|
|
separatorBuilder: (_, __) => SizedBox(height: 12.h), |
|
|
|
itemBuilder: (_, index) { |
|
|
|
if (index < controller.items.length) { |
|
|
|
final item = controller.items[index]; |
|
|
|
return _buildGoodsCard(item, isDarkMode); |
|
|
|
} |
|
|
|
if (index == controller.items.length) { |
|
|
|
return _buildFeaturesSection(isDarkMode); |
|
|
|
} |
|
|
|
return _buildFooterSection(isDarkMode); |
|
|
|
}, |
|
|
|
children: children, |
|
|
|
); |
|
|
|
} |
|
|
|
return Stack( |
|
|
|
@ -71,92 +95,6 @@ class GoodsvipView extends GetView<GoodsvipController> { |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
Widget _buildGoodsCard(DBGoods item, bool isDarkMode) { |
|
|
|
return Container( |
|
|
|
padding: EdgeInsets.all(16.w), |
|
|
|
decoration: BoxDecoration( |
|
|
|
color: isDarkMode ? Colors.grey[900] : Colors.white, |
|
|
|
borderRadius: BorderRadius.circular(12.r), |
|
|
|
boxShadow: [ |
|
|
|
BoxShadow( |
|
|
|
color: isDarkMode |
|
|
|
? Colors.black.withOpacity(0.2) |
|
|
|
: Colors.black.withOpacity(0.05), |
|
|
|
blurRadius: 10, |
|
|
|
offset: const Offset(0, 2), |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
child: Row( |
|
|
|
children: [ |
|
|
|
Container( |
|
|
|
width: 42.w, |
|
|
|
height: 42.w, |
|
|
|
decoration: BoxDecoration( |
|
|
|
color: isDarkMode |
|
|
|
? Colors.amber[900]!.withOpacity(0.3) |
|
|
|
: Colors.amber[100]!, |
|
|
|
borderRadius: BorderRadius.circular(21.r), |
|
|
|
), |
|
|
|
child: Icon(Icons.workspace_premium, |
|
|
|
color: isDarkMode ? Colors.amber[300]! : Colors.amber[600]!), |
|
|
|
), |
|
|
|
SizedBox(width: 12.w), |
|
|
|
Expanded( |
|
|
|
child: Column( |
|
|
|
crossAxisAlignment: CrossAxisAlignment.start, |
|
|
|
children: [ |
|
|
|
Text(item.name, |
|
|
|
style: TextStyle( |
|
|
|
fontSize: 16.sp, |
|
|
|
fontWeight: FontWeight.w600, |
|
|
|
color: isDarkMode ? Colors.white : Colors.black, |
|
|
|
)), |
|
|
|
SizedBox(height: 4.h), |
|
|
|
Text(localizedGoodsDesc(item), |
|
|
|
style: TextStyle( |
|
|
|
fontSize: 12.sp, |
|
|
|
color: isDarkMode ? Colors.white70 : Colors.grey[600], |
|
|
|
)), |
|
|
|
], |
|
|
|
), |
|
|
|
), |
|
|
|
SizedBox(width: 12.w), |
|
|
|
Column( |
|
|
|
crossAxisAlignment: CrossAxisAlignment.end, |
|
|
|
children: [ |
|
|
|
Text('¥${item.price.toStringAsFixed(2)}', |
|
|
|
style: TextStyle( |
|
|
|
fontSize: 16.sp, |
|
|
|
fontWeight: FontWeight.w600, |
|
|
|
color: isDarkMode ? Colors.white : Colors.black, |
|
|
|
)), |
|
|
|
SizedBox(height: 8.h), |
|
|
|
GestureDetector( |
|
|
|
onTap: () => controller.purchase(item), |
|
|
|
child: Container( |
|
|
|
padding: |
|
|
|
EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h), |
|
|
|
decoration: BoxDecoration( |
|
|
|
color: Colors.blue[600], |
|
|
|
borderRadius: BorderRadius.circular(20.r), |
|
|
|
), |
|
|
|
child: Text( |
|
|
|
'purchase'.tr, |
|
|
|
style: TextStyle( |
|
|
|
fontSize: 12.sp, |
|
|
|
color: Colors.white, |
|
|
|
), |
|
|
|
), |
|
|
|
), |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
Widget _buildFeaturesSection(bool isDarkMode) { |
|
|
|
final textColor = isDarkMode ? Colors.white : Colors.black; |
|
|
|
final subColor = isDarkMode ? Colors.white70 : Colors.grey[700]; |
|
|
|
@ -317,4 +255,674 @@ class GoodsvipView extends GetView<GoodsvipController> { |
|
|
|
), |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
Widget _buildTypeHeader(UsageType type, bool isDarkMode) { |
|
|
|
final titles = { |
|
|
|
UsageType.AI: 'aiFunction'.tr, |
|
|
|
UsageType.Meet: 'conferenceAssistant'.tr, |
|
|
|
UsageType.Translation: 'simultaneousTranslation'.tr, |
|
|
|
UsageType.Vip: 'premiumMember'.tr, |
|
|
|
}; |
|
|
|
final iconAndColor = _iconColorForType(type, isDarkMode); |
|
|
|
final subColor = isDarkMode ? Colors.white70 : Colors.grey[700]; |
|
|
|
String titleStr = titles[type] ?? type.name; |
|
|
|
String? vipSubtitle; |
|
|
|
if (type == UsageType.Vip) { |
|
|
|
int viplv = 0; |
|
|
|
int vipexptime = 0; |
|
|
|
if (User.isLoggedIn()) { |
|
|
|
final u = User.instance; |
|
|
|
viplv = u.viplv; |
|
|
|
vipexptime = u.vipexptime; |
|
|
|
} |
|
|
|
DateTime? expiry; |
|
|
|
if (vipexptime > 0) { |
|
|
|
expiry = vipexptime > 1000000000000 |
|
|
|
? DateTime.fromMillisecondsSinceEpoch(vipexptime) |
|
|
|
: DateTime.fromMillisecondsSinceEpoch(vipexptime * 1000); |
|
|
|
} |
|
|
|
titleStr = 'Vip $viplv'; |
|
|
|
vipSubtitle = expiry != null ? _fmtDate(expiry) : '未开启'; |
|
|
|
} |
|
|
|
return Container( |
|
|
|
padding: EdgeInsets.all(16.w), |
|
|
|
decoration: BoxDecoration( |
|
|
|
color: isDarkMode ? Colors.grey[900] : Colors.white, |
|
|
|
borderRadius: BorderRadius.circular(12.r), |
|
|
|
boxShadow: [ |
|
|
|
BoxShadow( |
|
|
|
color: isDarkMode |
|
|
|
? Colors.black.withOpacity(0.2) |
|
|
|
: Colors.black.withOpacity(0.05), |
|
|
|
blurRadius: 10, |
|
|
|
offset: const Offset(0, 2), |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
child: Row( |
|
|
|
crossAxisAlignment: CrossAxisAlignment.start, |
|
|
|
children: [ |
|
|
|
// Container( |
|
|
|
// width: 44.w, |
|
|
|
// height: 44.w, |
|
|
|
// decoration: BoxDecoration( |
|
|
|
// color: iconAndColor.background, |
|
|
|
// borderRadius: BorderRadius.circular(22.r), |
|
|
|
// ), |
|
|
|
// child: Icon(iconAndColor.icon, color: iconAndColor.foreground), |
|
|
|
// ), |
|
|
|
// SizedBox(width: 12.w), |
|
|
|
Expanded( |
|
|
|
child: Column( |
|
|
|
crossAxisAlignment: CrossAxisAlignment.start, |
|
|
|
children: [ |
|
|
|
Text( |
|
|
|
titleStr, |
|
|
|
style: TextStyle( |
|
|
|
fontSize: 18.sp, |
|
|
|
fontWeight: FontWeight.w700, |
|
|
|
color: isDarkMode ? Colors.white : Colors.black, |
|
|
|
), |
|
|
|
), |
|
|
|
SizedBox(height: 8.h), |
|
|
|
if (type == UsageType.Vip && vipSubtitle != null) |
|
|
|
Text( |
|
|
|
vipSubtitle, |
|
|
|
style: TextStyle(fontSize: 12.sp, color: subColor), |
|
|
|
) |
|
|
|
else |
|
|
|
_buildMetricsInline(type, isDarkMode), |
|
|
|
], |
|
|
|
), |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
Widget _buildTypeSection( |
|
|
|
UsageType type, List<DBGoods> list, bool isDarkMode) { |
|
|
|
return Column( |
|
|
|
crossAxisAlignment: CrossAxisAlignment.start, |
|
|
|
children: [ |
|
|
|
_buildTypeHeader(type, isDarkMode), |
|
|
|
SizedBox(height: 12.h), |
|
|
|
_buildGoodsGrid(list, type, isDarkMode), |
|
|
|
], |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
Widget _buildMetricsInline(UsageType type, bool isDarkMode) { |
|
|
|
final textColor = isDarkMode ? Colors.white : Colors.black; |
|
|
|
final subColor = isDarkMode ? Colors.white70 : Colors.grey[700]; |
|
|
|
if (type == UsageType.Vip) { |
|
|
|
int viplv = 0; |
|
|
|
int vipexptime = 0; |
|
|
|
if (User.isLoggedIn()) { |
|
|
|
final u = User.instance; |
|
|
|
viplv = u.viplv; |
|
|
|
vipexptime = u.vipexptime; |
|
|
|
} |
|
|
|
DateTime? expiry; |
|
|
|
if (vipexptime > 0) { |
|
|
|
expiry = vipexptime > 1000000000000 |
|
|
|
? DateTime.fromMillisecondsSinceEpoch(vipexptime) |
|
|
|
: DateTime.fromMillisecondsSinceEpoch(vipexptime * 1000); |
|
|
|
} |
|
|
|
return Row( |
|
|
|
children: [ |
|
|
|
Expanded( |
|
|
|
child: Column( |
|
|
|
crossAxisAlignment: CrossAxisAlignment.start, |
|
|
|
children: [ |
|
|
|
Text( |
|
|
|
'Lv.$viplv', |
|
|
|
style: TextStyle( |
|
|
|
fontSize: 14.sp, |
|
|
|
fontWeight: FontWeight.w600, |
|
|
|
color: textColor, |
|
|
|
), |
|
|
|
), |
|
|
|
SizedBox(height: 4.h), |
|
|
|
Text( |
|
|
|
expiry != null |
|
|
|
? '${'validityPeriod'.tr}${_fmtDate(expiry)}' |
|
|
|
: 'notBound'.tr, |
|
|
|
style: TextStyle(fontSize: 12.sp, color: subColor), |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
), |
|
|
|
// Container( |
|
|
|
// padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 4.h), |
|
|
|
// decoration: BoxDecoration( |
|
|
|
// color: isDarkMode ? Colors.white10 : Colors.black12, |
|
|
|
// borderRadius: BorderRadius.circular(16.r), |
|
|
|
// ), |
|
|
|
// child: Text( |
|
|
|
// viplv > 0 ? 'enabled'.tr : 'disabled'.tr, |
|
|
|
// style: TextStyle(fontSize: 11.sp, color: subColor), |
|
|
|
// ), |
|
|
|
// ), |
|
|
|
], |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
int total = 0; |
|
|
|
int remain = 0; |
|
|
|
if (User.isLoggedIn()) { |
|
|
|
final u = User.instance; |
|
|
|
if (type == UsageType.AI) { |
|
|
|
total = u.aichattotalintegral; |
|
|
|
remain = u.aichatintegral; |
|
|
|
} else if (type == UsageType.Meet) { |
|
|
|
total = u.meettotalintegral; |
|
|
|
remain = u.meetintegral; |
|
|
|
} else if (type == UsageType.Translation) { |
|
|
|
total = u.tradetotalintegral; |
|
|
|
remain = u.tradeintegral; |
|
|
|
} |
|
|
|
} |
|
|
|
final progress = (total > 0) ? (remain / total).clamp(0.0, 1.0) : 0.0; |
|
|
|
return Column( |
|
|
|
crossAxisAlignment: CrossAxisAlignment.start, |
|
|
|
children: [ |
|
|
|
LinearProgressIndicator( |
|
|
|
value: progress, |
|
|
|
minHeight: 8.h, |
|
|
|
backgroundColor: isDarkMode ? Colors.grey[800] : Colors.grey[300], |
|
|
|
valueColor: AlwaysStoppedAnimation<Color>( |
|
|
|
isDarkMode ? Colors.blue[300]! : Colors.blue[600]!), |
|
|
|
), |
|
|
|
SizedBox(height: 8.h), |
|
|
|
Row( |
|
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween, |
|
|
|
children: [ |
|
|
|
Text( |
|
|
|
'${'totalCount'.tr}:$total', |
|
|
|
style: TextStyle(fontSize: 12.sp, color: subColor), |
|
|
|
), |
|
|
|
Text( |
|
|
|
'${'remaining'.tr}:$remain', |
|
|
|
style: TextStyle(fontSize: 12.sp, color: subColor), |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
], |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
Widget _buildGoodsCardByType(DBGoods item, UsageType type, bool isDarkMode) { |
|
|
|
final iconAndColor = _iconColorForType(type, isDarkMode); |
|
|
|
return Container( |
|
|
|
padding: EdgeInsets.all(16.w), |
|
|
|
decoration: BoxDecoration( |
|
|
|
color: isDarkMode ? Colors.grey[900] : Colors.white, |
|
|
|
borderRadius: BorderRadius.circular(12.r), |
|
|
|
boxShadow: [ |
|
|
|
BoxShadow( |
|
|
|
color: isDarkMode |
|
|
|
? Colors.black.withOpacity(0.2) |
|
|
|
: Colors.black.withOpacity(0.05), |
|
|
|
blurRadius: 10, |
|
|
|
offset: const Offset(0, 2), |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
child: Row( |
|
|
|
children: [ |
|
|
|
Container( |
|
|
|
width: 42.w, |
|
|
|
height: 42.w, |
|
|
|
decoration: BoxDecoration( |
|
|
|
color: iconAndColor.background, |
|
|
|
borderRadius: BorderRadius.circular(21.r), |
|
|
|
), |
|
|
|
child: Icon(iconAndColor.icon, color: iconAndColor.foreground), |
|
|
|
), |
|
|
|
SizedBox(width: 12.w), |
|
|
|
Expanded( |
|
|
|
child: Column( |
|
|
|
crossAxisAlignment: CrossAxisAlignment.start, |
|
|
|
children: [ |
|
|
|
Text(localizedGoodsName(item), |
|
|
|
style: TextStyle( |
|
|
|
fontSize: 16.sp, |
|
|
|
fontWeight: FontWeight.w600, |
|
|
|
color: isDarkMode ? Colors.white : Colors.black, |
|
|
|
)), |
|
|
|
SizedBox(height: 4.h), |
|
|
|
Text(localizedGoodsDesc(item), |
|
|
|
style: TextStyle( |
|
|
|
fontSize: 12.sp, |
|
|
|
color: isDarkMode ? Colors.white70 : Colors.grey[600], |
|
|
|
)), |
|
|
|
], |
|
|
|
), |
|
|
|
), |
|
|
|
SizedBox(width: 12.w), |
|
|
|
Column( |
|
|
|
crossAxisAlignment: CrossAxisAlignment.end, |
|
|
|
children: [ |
|
|
|
Text('¥${item.priceYuan.toStringAsFixed(2)}', |
|
|
|
style: TextStyle( |
|
|
|
fontSize: 16.sp, |
|
|
|
fontWeight: FontWeight.w600, |
|
|
|
color: isDarkMode ? Colors.white : Colors.black, |
|
|
|
)), |
|
|
|
SizedBox(height: 8.h), |
|
|
|
GestureDetector( |
|
|
|
onTap: () => controller.purchase(item), |
|
|
|
child: Container( |
|
|
|
padding: |
|
|
|
EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h), |
|
|
|
decoration: BoxDecoration( |
|
|
|
color: Colors.blue[600], |
|
|
|
borderRadius: BorderRadius.circular(20.r), |
|
|
|
), |
|
|
|
child: Text( |
|
|
|
'purchase'.tr, |
|
|
|
style: TextStyle( |
|
|
|
fontSize: 12.sp, |
|
|
|
color: Colors.white, |
|
|
|
), |
|
|
|
), |
|
|
|
), |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
Widget _buildMetricsForType(UsageType type, bool isDarkMode) { |
|
|
|
if (type == UsageType.Vip) { |
|
|
|
return _buildVipMetricsCard(isDarkMode); |
|
|
|
} |
|
|
|
return _buildQuotaCard(type, isDarkMode); |
|
|
|
} |
|
|
|
|
|
|
|
Widget _buildVipMetricsCard(bool isDarkMode) { |
|
|
|
final textColor = isDarkMode ? Colors.white : Colors.black; |
|
|
|
final subColor = isDarkMode ? Colors.white70 : Colors.grey[700]; |
|
|
|
int viplv = 0; |
|
|
|
int vipexptime = 0; |
|
|
|
if (User.isLoggedIn()) { |
|
|
|
final u = User.instance; |
|
|
|
viplv = u.viplv; |
|
|
|
vipexptime = u.vipexptime; |
|
|
|
} |
|
|
|
DateTime? expiry; |
|
|
|
if (vipexptime > 0) { |
|
|
|
expiry = vipexptime > 1000000000000 |
|
|
|
? DateTime.fromMillisecondsSinceEpoch(vipexptime) |
|
|
|
: DateTime.fromMillisecondsSinceEpoch(vipexptime * 1000); |
|
|
|
} |
|
|
|
final iconColors = _iconColorForType(UsageType.Vip, isDarkMode); |
|
|
|
return Container( |
|
|
|
padding: EdgeInsets.all(16.w), |
|
|
|
decoration: BoxDecoration( |
|
|
|
color: isDarkMode ? Colors.grey[900] : Colors.white, |
|
|
|
borderRadius: BorderRadius.circular(12.r), |
|
|
|
boxShadow: [ |
|
|
|
BoxShadow( |
|
|
|
color: isDarkMode |
|
|
|
? Colors.black.withOpacity(0.2) |
|
|
|
: Colors.black.withOpacity(0.05), |
|
|
|
blurRadius: 10, |
|
|
|
offset: const Offset(0, 2), |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
child: Row( |
|
|
|
children: [ |
|
|
|
Container( |
|
|
|
width: 42.w, |
|
|
|
height: 42.w, |
|
|
|
decoration: BoxDecoration( |
|
|
|
color: iconColors.background, |
|
|
|
borderRadius: BorderRadius.circular(21.r), |
|
|
|
), |
|
|
|
child: Icon(iconColors.icon, color: iconColors.foreground), |
|
|
|
), |
|
|
|
SizedBox(width: 12.w), |
|
|
|
Expanded( |
|
|
|
child: Column( |
|
|
|
crossAxisAlignment: CrossAxisAlignment.start, |
|
|
|
children: [ |
|
|
|
Text( |
|
|
|
'Lv.$viplv', |
|
|
|
style: TextStyle( |
|
|
|
fontSize: 14.sp, |
|
|
|
fontWeight: FontWeight.w600, |
|
|
|
color: textColor, |
|
|
|
), |
|
|
|
), |
|
|
|
SizedBox(height: 4.h), |
|
|
|
Text( |
|
|
|
expiry != null |
|
|
|
? '${'validityPeriod'.tr}${_fmtDate(expiry)}' |
|
|
|
: 'notBound'.tr, |
|
|
|
style: TextStyle(fontSize: 12.sp, color: subColor), |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
), |
|
|
|
Container( |
|
|
|
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 4.h), |
|
|
|
decoration: BoxDecoration( |
|
|
|
color: isDarkMode ? Colors.white10 : Colors.black12, |
|
|
|
borderRadius: BorderRadius.circular(16.r), |
|
|
|
), |
|
|
|
child: Text( |
|
|
|
viplv > 0 ? 'enabled'.tr : 'disabled'.tr, |
|
|
|
style: TextStyle(fontSize: 11.sp, color: subColor), |
|
|
|
), |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
Widget _buildQuotaCard(UsageType type, bool isDarkMode) { |
|
|
|
int total = 0; |
|
|
|
int remain = 0; |
|
|
|
if (User.isLoggedIn()) { |
|
|
|
final u = User.instance; |
|
|
|
if (type == UsageType.AI) { |
|
|
|
total = u.aichattotalintegral; |
|
|
|
remain = u.aichatintegral; |
|
|
|
} else if (type == UsageType.Meet) { |
|
|
|
total = u.meettotalintegral; |
|
|
|
remain = u.meetintegral; |
|
|
|
} else if (type == UsageType.Translation) { |
|
|
|
total = u.tradetotalintegral; |
|
|
|
remain = u.tradeintegral; |
|
|
|
} |
|
|
|
} |
|
|
|
final progress = (total > 0) ? (remain / total).clamp(0.0, 1.0) : 0.0; |
|
|
|
final iconColors = _iconColorForType(type, isDarkMode); |
|
|
|
final subColor = isDarkMode ? Colors.white70 : Colors.grey[700]; |
|
|
|
final textColor = isDarkMode ? Colors.white : Colors.black; |
|
|
|
return Container( |
|
|
|
padding: EdgeInsets.all(16.w), |
|
|
|
decoration: BoxDecoration( |
|
|
|
color: isDarkMode ? Colors.grey[900] : Colors.white, |
|
|
|
borderRadius: BorderRadius.circular(12.r), |
|
|
|
boxShadow: [ |
|
|
|
BoxShadow( |
|
|
|
color: isDarkMode |
|
|
|
? Colors.black.withOpacity(0.2) |
|
|
|
: Colors.black.withOpacity(0.05), |
|
|
|
blurRadius: 10, |
|
|
|
offset: const Offset(0, 2), |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
child: Column( |
|
|
|
crossAxisAlignment: CrossAxisAlignment.start, |
|
|
|
children: [ |
|
|
|
Row( |
|
|
|
children: [ |
|
|
|
Container( |
|
|
|
width: 36.w, |
|
|
|
height: 36.w, |
|
|
|
decoration: BoxDecoration( |
|
|
|
color: iconColors.background, |
|
|
|
borderRadius: BorderRadius.circular(18.r), |
|
|
|
), |
|
|
|
child: Icon(iconColors.icon, color: iconColors.foreground), |
|
|
|
), |
|
|
|
SizedBox(width: 8.w), |
|
|
|
Text( |
|
|
|
'remainingQuota'.tr, |
|
|
|
style: TextStyle( |
|
|
|
fontSize: 16.sp, |
|
|
|
fontWeight: FontWeight.w600, |
|
|
|
color: textColor, |
|
|
|
), |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
SizedBox(height: 12.h), |
|
|
|
LinearProgressIndicator( |
|
|
|
value: progress, |
|
|
|
minHeight: 8.h, |
|
|
|
backgroundColor: isDarkMode ? Colors.grey[800] : Colors.grey[300], |
|
|
|
valueColor: AlwaysStoppedAnimation<Color>( |
|
|
|
isDarkMode ? Colors.blue[300]! : Colors.blue[600]!), |
|
|
|
), |
|
|
|
SizedBox(height: 8.h), |
|
|
|
Row( |
|
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween, |
|
|
|
children: [ |
|
|
|
Text( |
|
|
|
'${'totalCount'.tr}:$total', |
|
|
|
style: TextStyle(fontSize: 12.sp, color: subColor), |
|
|
|
), |
|
|
|
Text( |
|
|
|
'${'remaining'.tr}:$remain', |
|
|
|
style: TextStyle(fontSize: 12.sp, color: subColor), |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
Widget _buildGoodsGrid(List<DBGoods> list, UsageType type, bool isDarkMode) { |
|
|
|
return GridView.builder( |
|
|
|
shrinkWrap: true, |
|
|
|
physics: const NeverScrollableScrollPhysics(), |
|
|
|
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( |
|
|
|
crossAxisCount: 2, |
|
|
|
mainAxisSpacing: 12.h, |
|
|
|
crossAxisSpacing: 12.w, |
|
|
|
childAspectRatio: 1.15, |
|
|
|
), |
|
|
|
itemCount: list.length, |
|
|
|
itemBuilder: (_, i) => _buildGridItemCard(list[i], type, isDarkMode), |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
Widget _buildGridItemCard(DBGoods item, UsageType type, bool isDarkMode) { |
|
|
|
final iconColors = _iconColorForType(type, isDarkMode); |
|
|
|
return Container( |
|
|
|
padding: EdgeInsets.all(12.w), |
|
|
|
decoration: BoxDecoration( |
|
|
|
color: isDarkMode ? Colors.grey[900] : Colors.white, |
|
|
|
borderRadius: BorderRadius.circular(12.r), |
|
|
|
boxShadow: [ |
|
|
|
BoxShadow( |
|
|
|
color: isDarkMode |
|
|
|
? Colors.black.withOpacity(0.2) |
|
|
|
: Colors.black.withOpacity(0.05), |
|
|
|
blurRadius: 10, |
|
|
|
offset: const Offset(0, 2), |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
child: Column( |
|
|
|
crossAxisAlignment: CrossAxisAlignment.start, |
|
|
|
children: [ |
|
|
|
Row( |
|
|
|
children: [ |
|
|
|
Container( |
|
|
|
width: 34.w, |
|
|
|
height: 34.w, |
|
|
|
decoration: BoxDecoration( |
|
|
|
color: iconColors.background, |
|
|
|
borderRadius: BorderRadius.circular(17.r), |
|
|
|
), |
|
|
|
child: Icon(iconColors.icon, color: iconColors.foreground), |
|
|
|
), |
|
|
|
SizedBox(width: 8.w), |
|
|
|
Expanded( |
|
|
|
child: Text( |
|
|
|
localizedGoodsName(item), |
|
|
|
maxLines: 1, |
|
|
|
overflow: TextOverflow.ellipsis, |
|
|
|
style: TextStyle( |
|
|
|
fontSize: 14.sp, |
|
|
|
fontWeight: FontWeight.w600, |
|
|
|
color: isDarkMode ? Colors.white : Colors.black, |
|
|
|
), |
|
|
|
), |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
SizedBox(height: 6.h), |
|
|
|
Expanded( |
|
|
|
child: Text( |
|
|
|
localizedGoodsDesc(item), |
|
|
|
maxLines: 3, |
|
|
|
overflow: TextOverflow.ellipsis, |
|
|
|
style: TextStyle( |
|
|
|
fontSize: 12.sp, |
|
|
|
color: isDarkMode ? Colors.white70 : Colors.grey[700], |
|
|
|
), |
|
|
|
), |
|
|
|
), |
|
|
|
SizedBox(height: 6.h), |
|
|
|
Row( |
|
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween, |
|
|
|
children: [ |
|
|
|
Text( |
|
|
|
'¥${item.priceYuan.toStringAsFixed(2)}', |
|
|
|
style: TextStyle( |
|
|
|
fontSize: 14.sp, |
|
|
|
fontWeight: FontWeight.w600, |
|
|
|
color: isDarkMode ? Colors.white : Colors.black, |
|
|
|
), |
|
|
|
), |
|
|
|
GestureDetector( |
|
|
|
onTap: () => controller.purchase(item), |
|
|
|
child: Container( |
|
|
|
padding: |
|
|
|
EdgeInsets.symmetric(horizontal: 10.w, vertical: 6.h), |
|
|
|
decoration: BoxDecoration( |
|
|
|
color: Colors.blue[600], |
|
|
|
borderRadius: BorderRadius.circular(8.r), |
|
|
|
), |
|
|
|
child: Text( |
|
|
|
'purchase'.tr, |
|
|
|
style: TextStyle(fontSize: 12.sp, color: Colors.white), |
|
|
|
), |
|
|
|
), |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
String _fmtDate(DateTime d) { |
|
|
|
final y = d.year.toString().padLeft(4, '0'); |
|
|
|
final m = d.month.toString().padLeft(2, '0'); |
|
|
|
final day = d.day.toString().padLeft(2, '0'); |
|
|
|
return '$y-$m-$day'; |
|
|
|
} |
|
|
|
|
|
|
|
_IconColors _iconColorForType(UsageType type, bool isDarkMode) { |
|
|
|
switch (type) { |
|
|
|
case UsageType.AI: |
|
|
|
return _IconColors( |
|
|
|
icon: Icons.smart_toy, |
|
|
|
background: isDarkMode |
|
|
|
? Colors.teal[900]!.withOpacity(0.3) |
|
|
|
: Colors.teal[100]!, |
|
|
|
foreground: isDarkMode ? Colors.teal[300]! : Colors.teal[600]!, |
|
|
|
); |
|
|
|
case UsageType.Meet: |
|
|
|
return _IconColors( |
|
|
|
icon: Icons.people_alt, |
|
|
|
background: isDarkMode |
|
|
|
? Colors.pink[900]!.withOpacity(0.3) |
|
|
|
: Colors.pink[100]!, |
|
|
|
foreground: isDarkMode ? Colors.pink[300]! : Colors.pink[600]!, |
|
|
|
); |
|
|
|
case UsageType.Translation: |
|
|
|
return _IconColors( |
|
|
|
icon: Icons.translate, |
|
|
|
background: isDarkMode |
|
|
|
? Colors.indigo[900]!.withOpacity(0.3) |
|
|
|
: Colors.indigo[100]!, |
|
|
|
foreground: isDarkMode ? Colors.indigo[300]! : Colors.indigo[600]!, |
|
|
|
); |
|
|
|
case UsageType.Vip: |
|
|
|
case UsageType.UsageNull: |
|
|
|
return _IconColors( |
|
|
|
icon: Icons.workspace_premium, |
|
|
|
background: isDarkMode |
|
|
|
? Colors.amber[900]!.withOpacity(0.3) |
|
|
|
: Colors.amber[100]!, |
|
|
|
foreground: isDarkMode ? Colors.amber[300]! : Colors.amber[600]!, |
|
|
|
); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
Widget _buildScenesSection(bool isDarkMode) { |
|
|
|
final textColor = isDarkMode ? Colors.white : Colors.black; |
|
|
|
final subColor = isDarkMode ? Colors.white70 : Colors.grey[700]; |
|
|
|
return Column( |
|
|
|
crossAxisAlignment: CrossAxisAlignment.start, |
|
|
|
children: [ |
|
|
|
Text( |
|
|
|
'scene'.tr, |
|
|
|
style: TextStyle( |
|
|
|
fontSize: 16.sp, |
|
|
|
fontWeight: FontWeight.w600, |
|
|
|
color: textColor, |
|
|
|
), |
|
|
|
), |
|
|
|
SizedBox(height: 10.h), |
|
|
|
Wrap( |
|
|
|
spacing: 12.w, |
|
|
|
runSpacing: 12.h, |
|
|
|
children: [ |
|
|
|
_buildFeatureCard( |
|
|
|
isDarkMode, |
|
|
|
icon: Icons.smart_toy, |
|
|
|
title: 'aiFunction'.tr, |
|
|
|
lines: ['aiHeaderDesc1'.tr, 'aiHeaderDesc2'.tr], |
|
|
|
colorSeed: Colors.teal, |
|
|
|
), |
|
|
|
_buildFeatureCard( |
|
|
|
isDarkMode, |
|
|
|
icon: Icons.people_alt, |
|
|
|
title: 'conferenceAssistant'.tr, |
|
|
|
lines: ['meetingHeaderDesc1'.tr, 'meetingHeaderDesc2'.tr], |
|
|
|
colorSeed: Colors.pink, |
|
|
|
), |
|
|
|
_buildFeatureCard( |
|
|
|
isDarkMode, |
|
|
|
icon: Icons.translate, |
|
|
|
title: 'simultaneousTranslation'.tr, |
|
|
|
lines: ['tradeHeaderDesc1'.tr, 'tradeHeaderDesc2'.tr], |
|
|
|
colorSeed: Colors.indigo, |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
SizedBox(height: 12.h), |
|
|
|
Text( |
|
|
|
'vipDisclaimer'.tr, |
|
|
|
style: TextStyle(fontSize: 12.sp, color: subColor), |
|
|
|
), |
|
|
|
], |
|
|
|
); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
class _IconColors { |
|
|
|
final IconData icon; |
|
|
|
final Color background; |
|
|
|
final Color foreground; |
|
|
|
const _IconColors({ |
|
|
|
required this.icon, |
|
|
|
required this.background, |
|
|
|
required this.foreground, |
|
|
|
}); |
|
|
|
} |
|
|
|
|