18 changed files with 1849 additions and 455 deletions
@ -0,0 +1,206 @@ |
|||
/// 商品类型 |
|||
/// 0: 普通产品 normal |
|||
/// 1: 会员产品 member |
|||
// 使用量类型(与后端/Proto枚举对应) |
|||
enum UsageType { |
|||
/// 未知使用量类型 |
|||
UsageNull, // 0 |
|||
/// 翻译 |
|||
Trade, // 1 |
|||
/// 会议 |
|||
Meet, // 2 |
|||
/// 智能体 |
|||
AI; // 3 |
|||
|
|||
// 解析支持 int 或 string(大小写不敏感) |
|||
static UsageType from(dynamic value) { |
|||
if (value is int) { |
|||
switch (value) { |
|||
case 0: |
|||
return UsageType.UsageNull; |
|||
case 1: |
|||
return UsageType.Trade; |
|||
case 2: |
|||
return UsageType.Meet; |
|||
case 3: |
|||
return UsageType.AI; |
|||
default: |
|||
return UsageType.UsageNull; |
|||
} |
|||
} |
|||
if (value is String) { |
|||
final v = int.tryParse(value.trim()); |
|||
if (v != null) return UsageType.from(v); |
|||
switch (value.trim().toLowerCase()) { |
|||
case 'usagenull': |
|||
case 'null': |
|||
case 'unknown': |
|||
return UsageType.UsageNull; |
|||
case 'trade': |
|||
return UsageType.Trade; |
|||
case 'meet': |
|||
case 'meeting': |
|||
return UsageType.Meet; |
|||
case 'ai': |
|||
return UsageType.AI; |
|||
default: |
|||
return UsageType.UsageNull; |
|||
} |
|||
} |
|||
return UsageType.UsageNull; |
|||
} |
|||
|
|||
int get toInt { |
|||
switch (this) { |
|||
case UsageType.UsageNull: |
|||
return 0; |
|||
case UsageType.Trade: |
|||
return 1; |
|||
case UsageType.Meet: |
|||
return 2; |
|||
case UsageType.AI: |
|||
return 3; |
|||
} |
|||
} |
|||
|
|||
String get name { |
|||
switch (this) { |
|||
case UsageType.UsageNull: |
|||
return 'UsageNull'; |
|||
case UsageType.Trade: |
|||
return 'Trade'; |
|||
case UsageType.Meet: |
|||
return 'Meet'; |
|||
case UsageType.AI: |
|||
return 'AI'; |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// 支付商品模型,与后端 DBGoods 对应 |
|||
/// - id: 产品id |
|||
/// - enable: 是否启用 |
|||
/// - name: 产品名称 |
|||
/// - desc: 产品描述 |
|||
/// - price: 产品价格(建议单位:分) |
|||
/// - usagetype: 产品类型 0:普通产品 1:会员产品 |
|||
/// - usagenum: 使用量(时长/次数/额度等) |
|||
class DBGoods { |
|||
final String id; |
|||
final bool enable; |
|||
final String name; |
|||
final String desc; |
|||
final int price; // 建议单位:分 |
|||
final UsageType usagetype; |
|||
final int usagenum; |
|||
|
|||
const DBGoods({ |
|||
required this.id, |
|||
required this.enable, |
|||
required this.name, |
|||
required this.desc, |
|||
required this.price, |
|||
required this.usagetype, |
|||
required this.usagenum, |
|||
}); |
|||
|
|||
/// 解析后端 JSON |
|||
factory DBGoods.fromJson(Map<String, dynamic> json) { |
|||
String _asString(dynamic v) { |
|||
if (v == null) return ''; |
|||
return v is String ? v : v.toString(); |
|||
} |
|||
|
|||
bool _asBool(dynamic v) { |
|||
if (v is bool) return v; |
|||
if (v is num) return v != 0; |
|||
if (v is String) { |
|||
final s = v.trim().toLowerCase(); |
|||
return s == 'true' || s == '1'; |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
int _asInt(dynamic v) { |
|||
if (v == null) return 0; |
|||
if (v is int) return v; |
|||
if (v is num) return v.toInt(); |
|||
if (v is String) return int.tryParse(v) ?? 0; |
|||
return 0; |
|||
} |
|||
|
|||
return DBGoods( |
|||
id: _asString(json['id']), |
|||
enable: _asBool(json['enable']), |
|||
name: _asString(json['name']), |
|||
desc: _asString(json['desc']), |
|||
price: _asInt(json['price']), |
|||
usagetype: UsageType.from(json['usagetype']), |
|||
usagenum: _asInt(json['usagenum']), |
|||
); |
|||
} |
|||
|
|||
/// 编码为后端 JSON |
|||
Map<String, dynamic> toJson() { |
|||
return { |
|||
'id': id, |
|||
'enable': enable, |
|||
'name': name, |
|||
'desc': desc, |
|||
'price': price, |
|||
'usagetype': usagetype.toInt, |
|||
'usagenum': usagenum, |
|||
}; |
|||
} |
|||
|
|||
/// 列表解析 |
|||
static List<DBGoods> listFromJson(dynamic data) { |
|||
if (data == null) return const []; |
|||
if (data is List) { |
|||
return data |
|||
.map((e) => DBGoods.fromJson(e as Map<String, dynamic>)) |
|||
.toList(); |
|||
} |
|||
// 某些接口返回 { list: [...] } 或 { data: [...] } |
|||
if (data is Map<String, dynamic>) { |
|||
final list = data['list'] ?? data['data'] ?? data['items']; |
|||
if (list is List) { |
|||
return list |
|||
.map((e) => DBGoods.fromJson(e as Map<String, dynamic>)) |
|||
.toList(); |
|||
} |
|||
} |
|||
return const []; |
|||
} |
|||
|
|||
/// 价格换算:分 -> 元(仅当 price 单位为分时使用) |
|||
double get priceYuan => price / 100.0; |
|||
|
|||
/// 用于支付创建订单的金额(分) |
|||
int get payAmountCents => price; |
|||
|
|||
DBGoods copyWith({ |
|||
String? id, |
|||
bool? enable, |
|||
String? name, |
|||
String? desc, |
|||
int? price, |
|||
UsageType? usagetype, |
|||
int? usagenum, |
|||
}) { |
|||
return DBGoods( |
|||
id: id ?? this.id, |
|||
enable: enable ?? this.enable, |
|||
name: name ?? this.name, |
|||
desc: desc ?? this.desc, |
|||
price: price ?? this.price, |
|||
usagetype: usagetype ?? this.usagetype, |
|||
usagenum: usagenum ?? this.usagenum, |
|||
); |
|||
} |
|||
|
|||
@override |
|||
String toString() { |
|||
return 'DBGoods(id: $id, enable: $enable, name: $name, price: $price, usagetype: ${usagetype.name}, usagenum: $usagenum)'; |
|||
} |
|||
} |
|||
@ -0,0 +1,281 @@ |
|||
import 'package:flutter/material.dart'; |
|||
import 'package:get/get.dart'; |
|||
import '../../../routes/app_routes.dart'; |
|||
import '../../../data/services/pay_service.dart'; |
|||
import '../../../data/models/goods.dart'; |
|||
import 'package:flutter_dotenv/flutter_dotenv.dart'; |
|||
import '../../../data/services/network/dio_manager.dart'; |
|||
|
|||
/// goods 模块基础控制器,封装通用的商品加载与支付流程 |
|||
abstract class GoodsBaseController extends GetxController { |
|||
final RxList<DBGoods> items = <DBGoods>[].obs; |
|||
final RxBool loading = false.obs; |
|||
final RxString error = ''.obs; |
|||
|
|||
late final PayService _payService = Get.find<PayService>(); |
|||
|
|||
@override |
|||
void onInit() { |
|||
super.onInit(); |
|||
_init(); |
|||
} |
|||
|
|||
Future<void> _init() async { |
|||
loading.value = true; |
|||
error.value = ''; |
|||
try { |
|||
final list = await fetchProducts(); |
|||
items.assignAll(list); |
|||
} catch (e) { |
|||
error.value = e.toString(); |
|||
} finally { |
|||
loading.value = false; |
|||
} |
|||
} |
|||
|
|||
/// 子类实现:返回商品列表(统一使用 DBGoods) |
|||
Future<List<DBGoods>> fetchProducts(); |
|||
|
|||
/// 统一入口:不指定支付方式时,跳转到支付页面交由用户选择 |
|||
Future<void> purchase(DBGoods item) async { |
|||
final method = await _showPayMethodSheet(); |
|||
if (method == null) return; |
|||
|
|||
bool ok = false; |
|||
String failMsg = '支付失败,请重试'; |
|||
if (method == _PayMethod.wechat) { |
|||
ok = await payWechat(item, onResult: (success, msg) { |
|||
if (!success) failMsg = msg ?? failMsg; |
|||
}); |
|||
} else if (method == _PayMethod.alipay) { |
|||
ok = await payAlipay(item, onResult: (success, msg) { |
|||
if (!success) failMsg = msg ?? failMsg; |
|||
}); |
|||
} else if (method == _PayMethod.paypal) { |
|||
ok = await payPaypal(item, onResult: (success, msg) { |
|||
if (!success) failMsg = msg ?? failMsg; |
|||
}); |
|||
} |
|||
|
|||
if (!ok) { |
|||
Get.snackbar('提示', failMsg, |
|||
snackPosition: SnackPosition.BOTTOM, |
|||
margin: const EdgeInsets.all(12)); |
|||
} |
|||
} |
|||
|
|||
/// 底部弹窗:选择支付方式 |
|||
Future<_PayMethod?> _showPayMethodSheet() async { |
|||
final context = Get.context; |
|||
final isDark = |
|||
context != null && Theme.of(context).brightness == Brightness.dark; |
|||
return await Get.bottomSheet<_PayMethod>( |
|||
SafeArea( |
|||
child: Container( |
|||
decoration: BoxDecoration( |
|||
color: isDark ? Colors.grey[900] : Colors.white, |
|||
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), |
|||
), |
|||
child: Column( |
|||
mainAxisSize: MainAxisSize.min, |
|||
children: [ |
|||
Padding( |
|||
padding: const EdgeInsets.symmetric(vertical: 12), |
|||
child: Container( |
|||
width: 40, |
|||
height: 4, |
|||
decoration: BoxDecoration( |
|||
color: isDark ? Colors.white24 : Colors.black12, |
|||
borderRadius: BorderRadius.circular(2), |
|||
), |
|||
), |
|||
), |
|||
Padding( |
|||
padding: |
|||
const EdgeInsets.symmetric(horizontal: 16, vertical: 8), |
|||
child: Column( |
|||
crossAxisAlignment: CrossAxisAlignment.center, |
|||
children: [ |
|||
Text( |
|||
'选择支付方式', |
|||
style: TextStyle( |
|||
fontSize: 16, |
|||
fontWeight: FontWeight.w600, |
|||
color: isDark ? Colors.white : Colors.black, |
|||
), |
|||
), |
|||
const SizedBox(height: 12), |
|||
Row( |
|||
mainAxisAlignment: MainAxisAlignment.spaceEvenly, |
|||
children: [ |
|||
GestureDetector( |
|||
onTap: () => Get.back(result: _PayMethod.wechat), |
|||
child: Column( |
|||
mainAxisSize: MainAxisSize.min, |
|||
children: [ |
|||
Container( |
|||
width: 56, |
|||
height: 56, |
|||
decoration: BoxDecoration( |
|||
shape: BoxShape.circle, |
|||
color: |
|||
isDark ? Colors.white10 : Colors.black12, |
|||
border: Border.all( |
|||
color: isDark |
|||
? Colors.white24 |
|||
: Colors.black12, |
|||
), |
|||
), |
|||
child: Icon(Icons.wechat, |
|||
color: Colors.green[600]), |
|||
), |
|||
const SizedBox(height: 8), |
|||
Text( |
|||
'微信支付', |
|||
style: TextStyle( |
|||
fontSize: 13, |
|||
color: isDark ? Colors.white : Colors.black, |
|||
), |
|||
), |
|||
], |
|||
), |
|||
), |
|||
GestureDetector( |
|||
onTap: () => Get.back(result: _PayMethod.alipay), |
|||
child: Column( |
|||
mainAxisSize: MainAxisSize.min, |
|||
children: [ |
|||
Container( |
|||
width: 56, |
|||
height: 56, |
|||
decoration: BoxDecoration( |
|||
shape: BoxShape.circle, |
|||
color: |
|||
isDark ? Colors.white10 : Colors.black12, |
|||
border: Border.all( |
|||
color: isDark |
|||
? Colors.white24 |
|||
: Colors.black12, |
|||
), |
|||
), |
|||
child: Icon(Icons.payment, |
|||
color: Colors.blue[600]), |
|||
), |
|||
const SizedBox(height: 8), |
|||
Text( |
|||
'支付宝', |
|||
style: TextStyle( |
|||
fontSize: 13, |
|||
color: isDark ? Colors.white : Colors.black, |
|||
), |
|||
), |
|||
], |
|||
), |
|||
), |
|||
GestureDetector( |
|||
onTap: () => Get.back(result: _PayMethod.paypal), |
|||
child: Column( |
|||
mainAxisSize: MainAxisSize.min, |
|||
children: [ |
|||
Container( |
|||
width: 56, |
|||
height: 56, |
|||
decoration: BoxDecoration( |
|||
shape: BoxShape.circle, |
|||
color: |
|||
isDark ? Colors.white10 : Colors.black12, |
|||
border: Border.all( |
|||
color: isDark |
|||
? Colors.white24 |
|||
: Colors.black12, |
|||
), |
|||
), |
|||
child: Icon( |
|||
Icons.account_balance_wallet, |
|||
color: Colors.indigo[600], |
|||
), |
|||
), |
|||
const SizedBox(height: 8), |
|||
Text( |
|||
'PayPal', |
|||
style: TextStyle( |
|||
fontSize: 13, |
|||
color: isDark ? Colors.white : Colors.black, |
|||
), |
|||
), |
|||
], |
|||
), |
|||
), |
|||
], |
|||
), |
|||
const SizedBox(height: 12), |
|||
], |
|||
), |
|||
), |
|||
], |
|||
), |
|||
), |
|||
), |
|||
isScrollControlled: false, |
|||
ignoreSafeArea: false, |
|||
backgroundColor: Colors.transparent, |
|||
); |
|||
} |
|||
|
|||
/// 指定微信支付(直接下单并拉起微信) |
|||
Future<bool> payWechat(DBGoods item, |
|||
{PaymentResultCallback? onResult}) async { |
|||
final cents = item.price; // DBGoods.price 单位:分 |
|||
final serverUrl = dotenv.env['SERVER_URL'] ?? DioManager().baseUrl; |
|||
final paymentData = await _payService.createWechatPayOrder( |
|||
productId: item.id, |
|||
productName: item.name, |
|||
productDesc: item.desc, |
|||
amount: cents, |
|||
notifyUrl: '$serverUrl/api/home/pay_wechatnotify', |
|||
); |
|||
if (paymentData == null) { |
|||
onResult?.call(false, '创建微信订单失败'); |
|||
return false; |
|||
} |
|||
return _payService.wechatPay(paymentData: paymentData, onResult: onResult); |
|||
} |
|||
|
|||
/// 指定支付宝支付(直接下单并拉起支付宝) |
|||
Future<bool> payAlipay(DBGoods item, |
|||
{PaymentResultCallback? onResult}) async { |
|||
final cents = item.price; // DBGoods.price 单位:分 |
|||
final orderInfo = await _payService.createAlipayOrder( |
|||
productId: item.id, |
|||
productName: item.name, |
|||
productDesc: item.desc, |
|||
amount: cents, |
|||
notifyUrl: 'https://api.ideapsound.com/api/home/pay_alipaynotify', |
|||
); |
|||
if (orderInfo == null) { |
|||
onResult?.call(false, '创建支付宝订单失败'); |
|||
return false; |
|||
} |
|||
return _payService.alipay(orderInfo: orderInfo, onResult: onResult); |
|||
} |
|||
|
|||
/// 指定 PayPal 支付(创建订单并打开授权链接) |
|||
Future<bool> payPaypal(DBGoods item, |
|||
{PaymentResultCallback? onResult}) async { |
|||
final cents = item.price; // DBGoods.price 单位:分 |
|||
final approvalUrl = await _payService.createPaypalOrder( |
|||
productId: item.id, |
|||
productName: item.name, |
|||
productDesc: item.desc, |
|||
amount: cents, |
|||
notifyUrl: 'https://api.ideapsound.com/api/home/pay_paypalnotify', |
|||
); |
|||
if (approvalUrl == null) { |
|||
onResult?.call(false, '创建 PayPal 订单失败'); |
|||
return false; |
|||
} |
|||
return _payService.paypalPay(approvalUrl: approvalUrl, onResult: onResult); |
|||
} |
|||
} |
|||
|
|||
enum _PayMethod { wechat, alipay, paypal } |
|||
@ -1,65 +1,40 @@ |
|||
import 'dart:convert'; |
|||
import 'dart:developer' as developer; |
|||
|
|||
import 'package:get/get.dart'; |
|||
import '../../../routes/app_routes.dart'; |
|||
import '../../../data/models/goods.dart'; |
|||
import '../../../data/services/network/api.dart'; |
|||
import 'goods_base_controller.dart'; |
|||
// ... existing code ... |
|||
|
|||
class AIItem { |
|||
final String id; |
|||
final String title; |
|||
final String description; |
|||
final double price; |
|||
final String productId; |
|||
|
|||
AIItem({ |
|||
required this.id, |
|||
required this.title, |
|||
required this.description, |
|||
required this.price, |
|||
required this.productId, |
|||
}); |
|||
} |
|||
|
|||
class GoodsaiController extends GetxController { |
|||
final RxList<AIItem> items = <AIItem>[].obs; |
|||
final RxBool loading = false.obs; |
|||
final RxString error = ''.obs; |
|||
|
|||
class GoodsaiController extends GoodsBaseController { |
|||
@override |
|||
void onInit() { |
|||
super.onInit(); |
|||
_loadProducts(); |
|||
} |
|||
|
|||
Future<void> _loadProducts() async { |
|||
loading.value = true; |
|||
error.value = ''; |
|||
Future<List<DBGoods>> fetchProducts() async { |
|||
final resp = await Api.getGoods({'usagetype': UsageType.AI.toInt}); |
|||
// 输出完整响应,便于调试接口结构 |
|||
try { |
|||
items.assignAll([ |
|||
AIItem( |
|||
id: 'ai_chat', |
|||
title: '通用AI聊天包', |
|||
description: '更长对话额度与工具调用', |
|||
price: 19.9, |
|||
productId: 'ai_chat_101', |
|||
), |
|||
AIItem( |
|||
id: 'ai_tools', |
|||
title: 'AI工具扩展', |
|||
description: '额外工具能力与上下文长度', |
|||
price: 39.9, |
|||
productId: 'ai_tools_201', |
|||
), |
|||
]); |
|||
} catch (e) { |
|||
error.value = e.toString(); |
|||
} finally { |
|||
loading.value = false; |
|||
developer.log('getGoods(AI) resp: ${jsonEncode(resp)}'); |
|||
} catch (_) { |
|||
developer.log('getGoods(AI) resp (raw): $resp'); |
|||
} |
|||
if (resp == null || resp is! Map) return []; |
|||
if (resp.containsKey('error') || resp.containsKey('msg')) { |
|||
final msg = resp['error'] ?? resp['msg'] ?? '服务器错误'; |
|||
throw Exception(msg); |
|||
} |
|||
final container = |
|||
(resp is Map && resp['data'] is Map) ? resp['data'] : resp; |
|||
final data = (container is Map) ? (container['goodss']) : null; |
|||
if (data is List) { |
|||
final list = |
|||
data.whereType<Map<String, dynamic>>().map(DBGoods.fromJson).toList(); |
|||
// 兜底过滤确保展示 AI 类型 |
|||
return list.where((g) => g.usagetype == UsageType.AI).toList(); |
|||
} |
|||
return []; |
|||
} |
|||
|
|||
void purchase(AIItem item) { |
|||
Get.toNamed(Routes.payment, arguments: { |
|||
'productId': item.productId, |
|||
'amount': item.price, |
|||
'description': item.title, |
|||
}); |
|||
} |
|||
} |
|||
// 如需自定义跳转逻辑,可保留重写调用父类 |
|||
// Future<void> purchase(DBGoods item) async => super.purchase(item); |
|||
} |
|||
|
|||
@ -1,65 +1,30 @@ |
|||
import 'package:get/get.dart'; |
|||
import '../../../routes/app_routes.dart'; |
|||
import '../../../data/models/goods.dart'; |
|||
import '../../../data/services/network/api.dart'; |
|||
import 'goods_base_controller.dart'; |
|||
|
|||
class MeetItem { |
|||
final String id; |
|||
final String title; |
|||
final String description; |
|||
final double price; |
|||
final String productId; |
|||
|
|||
MeetItem({ |
|||
required this.id, |
|||
required this.title, |
|||
required this.description, |
|||
required this.price, |
|||
required this.productId, |
|||
}); |
|||
} |
|||
|
|||
class GoodsmeetController extends GetxController { |
|||
final RxList<MeetItem> items = <MeetItem>[].obs; |
|||
final RxBool loading = false.obs; |
|||
final RxString error = ''.obs; |
|||
|
|||
class GoodsmeetController extends GoodsBaseController { |
|||
@override |
|||
void onInit() { |
|||
super.onInit(); |
|||
_loadProducts(); |
|||
} |
|||
|
|||
Future<void> _loadProducts() async { |
|||
loading.value = true; |
|||
error.value = ''; |
|||
try { |
|||
items.assignAll([ |
|||
MeetItem( |
|||
id: 'meet_basic', |
|||
title: '会议助手基础版', |
|||
description: '会议纪要与基础翻译', |
|||
price: 29.9, |
|||
productId: 'meet_101', |
|||
), |
|||
MeetItem( |
|||
id: 'meet_pro', |
|||
title: '会议助手高级版', |
|||
description: '纪要、翻译与AI总结增强', |
|||
price: 59.9, |
|||
productId: 'meet_201', |
|||
), |
|||
]); |
|||
} catch (e) { |
|||
error.value = e.toString(); |
|||
} finally { |
|||
loading.value = false; |
|||
Future<List<DBGoods>> fetchProducts() async { |
|||
final resp = await Api.getGoods({'usagetype': UsageType.Meet.toInt}); |
|||
if (resp == null || resp is! Map) return []; |
|||
if (resp.containsKey('error') || resp.containsKey('msg')) { |
|||
final msg = resp['error'] ?? resp['msg'] ?? '服务器错误'; |
|||
throw Exception(msg); |
|||
} |
|||
final container = |
|||
(resp is Map && resp['data'] is Map) ? resp['data'] : resp; |
|||
final data = (container is Map) |
|||
? (container['goodss'] ?? container['goodes'] ?? container['goods']) |
|||
: null; |
|||
if (data is List) { |
|||
final list = |
|||
data.whereType<Map<String, dynamic>>().map(DBGoods.fromJson).toList(); |
|||
return list.where((g) => g.usagetype == UsageType.Meet).toList(); |
|||
} |
|||
return []; |
|||
} |
|||
|
|||
void purchase(MeetItem item) { |
|||
Get.toNamed(Routes.payment, arguments: { |
|||
'productId': item.productId, |
|||
'amount': item.price, |
|||
'description': item.title, |
|||
}); |
|||
} |
|||
} |
|||
// Future<void> purchase(DBGoods item) async => super.purchase(item); |
|||
} |
|||
|
|||
@ -1,65 +1,30 @@ |
|||
import 'package:get/get.dart'; |
|||
import '../../../routes/app_routes.dart'; |
|||
import '../../../data/models/goods.dart'; |
|||
import '../../../data/services/network/api.dart'; |
|||
import 'goods_base_controller.dart'; |
|||
|
|||
class TradeItem { |
|||
final String id; |
|||
final String title; |
|||
final String description; |
|||
final double price; |
|||
final String productId; |
|||
|
|||
TradeItem({ |
|||
required this.id, |
|||
required this.title, |
|||
required this.description, |
|||
required this.price, |
|||
required this.productId, |
|||
}); |
|||
} |
|||
|
|||
class GoodstradeController extends GetxController { |
|||
final RxList<TradeItem> items = <TradeItem>[].obs; |
|||
final RxBool loading = false.obs; |
|||
final RxString error = ''.obs; |
|||
|
|||
class GoodstradeController extends GoodsBaseController { |
|||
@override |
|||
void onInit() { |
|||
super.onInit(); |
|||
_loadProducts(); |
|||
} |
|||
|
|||
Future<void> _loadProducts() async { |
|||
loading.value = true; |
|||
error.value = ''; |
|||
try { |
|||
items.assignAll([ |
|||
TradeItem( |
|||
id: 'trans_100', |
|||
title: '翻译包 100 分钟', |
|||
description: '实时翻译服务时长包', |
|||
price: 19.9, |
|||
productId: 'trans_100', |
|||
), |
|||
TradeItem( |
|||
id: 'trans_500', |
|||
title: '翻译包 500 分钟', |
|||
description: '更划算的翻译时长包', |
|||
price: 79.9, |
|||
productId: 'trans_500', |
|||
), |
|||
]); |
|||
} catch (e) { |
|||
error.value = e.toString(); |
|||
} finally { |
|||
loading.value = false; |
|||
Future<List<DBGoods>> fetchProducts() async { |
|||
final resp = await Api.getGoods({'usagetype': UsageType.Trade.toInt}); |
|||
if (resp == null || resp is! Map) return []; |
|||
if (resp.containsKey('error') || resp.containsKey('msg')) { |
|||
final msg = resp['error'] ?? resp['msg'] ?? '服务器错误'; |
|||
throw Exception(msg); |
|||
} |
|||
final container = |
|||
(resp is Map && resp['data'] is Map) ? resp['data'] : resp; |
|||
final data = (container is Map) |
|||
? (container['goodss'] ?? container['goodes'] ?? container['goods']) |
|||
: null; |
|||
if (data is List) { |
|||
final list = |
|||
data.whereType<Map<String, dynamic>>().map(DBGoods.fromJson).toList(); |
|||
return list.where((g) => g.usagetype == UsageType.Trade).toList(); |
|||
} |
|||
return []; |
|||
} |
|||
|
|||
void purchase(TradeItem item) { |
|||
Get.toNamed(Routes.payment, arguments: { |
|||
'productId': item.productId, |
|||
'amount': item.price, |
|||
'description': item.title, |
|||
}); |
|||
} |
|||
} |
|||
// Future<void> purchase(DBGoods item) async => super.purchase(item); |
|||
} |
|||
|
|||
@ -1,66 +1,30 @@ |
|||
import 'package:get/get.dart'; |
|||
import '../../../routes/app_routes.dart'; |
|||
import '../../../data/models/goods.dart'; |
|||
import '../../../data/services/network/api.dart'; |
|||
import 'goods_base_controller.dart'; |
|||
|
|||
class GoodsItem { |
|||
final String id; |
|||
final String title; |
|||
final String description; |
|||
final double price; |
|||
final String productId; |
|||
|
|||
GoodsItem({ |
|||
required this.id, |
|||
required this.title, |
|||
required this.description, |
|||
required this.price, |
|||
required this.productId, |
|||
}); |
|||
} |
|||
|
|||
class GoodsvipController extends GetxController { |
|||
final RxList<GoodsItem> items = <GoodsItem>[].obs; |
|||
final RxBool loading = false.obs; |
|||
final RxString error = ''.obs; |
|||
|
|||
class GoodsvipController extends GoodsBaseController { |
|||
@override |
|||
void onInit() { |
|||
super.onInit(); |
|||
_loadProducts(); |
|||
} |
|||
|
|||
Future<void> _loadProducts() async { |
|||
loading.value = true; |
|||
error.value = ''; |
|||
try { |
|||
// 示例数据,可接后端商品接口替换 |
|||
items.assignAll([ |
|||
GoodsItem( |
|||
id: 'vip_month', |
|||
title: '月度会员', |
|||
description: '解锁高级功能,按月订阅', |
|||
price: 9.9, |
|||
productId: 'vip_101', |
|||
), |
|||
GoodsItem( |
|||
id: 'vip_year', |
|||
title: '年度会员', |
|||
description: '解锁高级功能,按年订阅', |
|||
price: 99.0, |
|||
productId: 'vip_201', |
|||
), |
|||
]); |
|||
} catch (e) { |
|||
error.value = e.toString(); |
|||
} finally { |
|||
loading.value = false; |
|||
Future<List<DBGoods>> fetchProducts() async { |
|||
final resp = await Api.getGoods({'usagetype': UsageType.UsageNull.toInt}); |
|||
if (resp == null || resp is! Map) return []; |
|||
if (resp.containsKey('error') || resp.containsKey('msg')) { |
|||
final msg = resp['error'] ?? resp['msg'] ?? '服务器错误'; |
|||
throw Exception(msg); |
|||
} |
|||
final container = |
|||
(resp is Map && resp['data'] is Map) ? resp['data'] : resp; |
|||
final data = (container is Map) |
|||
? (container['goodss'] ?? container['goodes'] ?? container['goods']) |
|||
: null; |
|||
if (data is List) { |
|||
final list = |
|||
data.whereType<Map<String, dynamic>>().map(DBGoods.fromJson).toList(); |
|||
return list.where((g) => g.usagetype == UsageType.UsageNull).toList(); |
|||
} |
|||
return []; |
|||
} |
|||
|
|||
void purchase(GoodsItem item) { |
|||
Get.toNamed(Routes.payment, arguments: { |
|||
'productId': item.productId, |
|||
'amount': item.price, |
|||
'description': item.title, |
|||
}); |
|||
} |
|||
} |
|||
// Future<void> purchase(DBGoods item) async => super.purchase(item); |
|||
} |
|||
|
|||
@ -1,157 +1,2 @@ |
|||
import 'dart:ffi'; |
|||
|
|||
import 'package:flutter/services.dart'; |
|||
|
|||
class MusicService { |
|||
static final MusicService _instance = MusicService._internal(); |
|||
|
|||
/// 获取单例 |
|||
factory MusicService() => _instance; |
|||
|
|||
// 方法通道 |
|||
final MethodChannel _methodChannel = const MethodChannel( |
|||
'com.yunqiinnovation.music_service', |
|||
); |
|||
|
|||
// 事件通道 - 简化为只使用数据和状态通道 |
|||
late final EventChannel _dataEventChannel; |
|||
late final EventChannel _statusEventChannel; |
|||
|
|||
// 事件流 |
|||
late final Stream<dynamic> _dataStream; |
|||
late final Stream<dynamic> _statusStream; |
|||
|
|||
// 获取事件流 |
|||
Stream<dynamic> get onDataReceived => _dataStream; |
|||
|
|||
// 状态流直接传递原始数据 |
|||
Stream<dynamic> get onStatusUpdated => _statusStream; |
|||
|
|||
// 内部构造函数 |
|||
MusicService._internal() { |
|||
_dataEventChannel = const EventChannel( |
|||
'com.yunqiinnovation.music_service/data_events', |
|||
); |
|||
_statusEventChannel = const EventChannel( |
|||
'com.yunqiinnovation.music_service/status_events', |
|||
); |
|||
|
|||
// 初始化事件流 |
|||
_dataStream = _dataEventChannel.receiveBroadcastStream(); |
|||
_statusStream = _statusEventChannel.receiveBroadcastStream(); |
|||
} |
|||
|
|||
Future<void> playMusic(Map<String, dynamic> song) async { |
|||
await _methodChannel.invokeMethod('play', {'song': song}); |
|||
} |
|||
|
|||
Future<void> pauseMusic() async => _methodChannel.invokeMethod('pause'); |
|||
Future<void> resumeMusic() async => _methodChannel.invokeMethod('resume'); |
|||
Future<void> nextTrack() async => _methodChannel.invokeMethod('next'); |
|||
Future<void> previousTrack() async => _methodChannel.invokeMethod('previous'); |
|||
Future<void> stopMusic() async => _methodChannel.invokeMethod('stop'); |
|||
Future<void> setVolume(double volume) async => _methodChannel.invokeMethod( |
|||
'setVolume', |
|||
{'volume': volume}, |
|||
); // volume: 0.0~1.0 |
|||
Future<Map<String, dynamic>> getStatus() async { |
|||
final result = await _methodChannel.invokeMethod<Map>('getStatus'); |
|||
return { |
|||
'isPlaying': result?['isPlaying'] ?? false, |
|||
'id': result?['id'] ?? 0, |
|||
}; |
|||
} |
|||
|
|||
/// 获取播放进度信息 |
|||
Future<Map<String, dynamic>> getProgress() async { |
|||
final result = await _methodChannel.invokeMethod<Map>('getProgress'); |
|||
return { |
|||
'currentPosition': result?['currentPosition'] ?? 0, // 当前播放位置(毫秒) |
|||
'duration': result?['duration'] ?? 0, // 总时长(毫秒) |
|||
'progress': result?['progress'] ?? 0.0, // 播放进度(0.0-1.0) |
|||
}; |
|||
} |
|||
|
|||
/// 跳转到指定位置 |
|||
Future<void> seekTo(int positionMs) async { |
|||
await _methodChannel.invokeMethod('seekTo', {'position': positionMs}); |
|||
} |
|||
|
|||
/// 播放音乐列表 |
|||
Future<void> playMusicPlaylist(List<Map<String, dynamic>> songs) async { |
|||
await _methodChannel.invokeMethod('playlist', {'songs': songs}); |
|||
} |
|||
|
|||
/// 更新媒体会话进度信息 |
|||
Future<void> updateMediaSessionProgress( |
|||
int currentPosition, |
|||
int duration, |
|||
double progress, |
|||
) async { |
|||
await _methodChannel.invokeMethod('updateMediaSessionProgress', { |
|||
'currentPosition': currentPosition, |
|||
'duration': duration, |
|||
'progress': progress, |
|||
}); |
|||
} |
|||
|
|||
/// 设置媒体会话回调 |
|||
Future<void> setMediaSessionCallbacks({ |
|||
required Function() onPlay, |
|||
required Function() onPause, |
|||
required Function() onNext, |
|||
required Function() onPrevious, |
|||
required Function(int) onSeekTo, |
|||
required Function() onStop, |
|||
required Function(int, int, int, double) onProgress, |
|||
}) async { |
|||
// 设置方法调用处理器来处理来自原生平台的回调 |
|||
_methodChannel.setMethodCallHandler((call) async { |
|||
switch (call.method) { |
|||
case 'onMediaSessionPlay': |
|||
onPlay(); |
|||
break; |
|||
case 'onMediaSessionPause': |
|||
onPause(); |
|||
break; |
|||
case 'onMediaSessionNext': |
|||
onNext(); |
|||
break; |
|||
case 'onMediaSessionPrevious': |
|||
onPrevious(); |
|||
break; |
|||
case 'onMediaSessionSeekTo': |
|||
final position = call.arguments['position'] as int; |
|||
onSeekTo(position); |
|||
break; |
|||
case 'onMediaSessionStop': |
|||
onStop(); |
|||
break; |
|||
case 'onMediaSessionProgress': |
|||
final currentIdInternal = call.arguments['currentIdInternal'] as int; |
|||
final currentPosition = call.arguments['currentPosition'] as int; |
|||
final duration = call.arguments['duration'] as int; |
|||
final progress = call.arguments['progress'] as double; |
|||
onProgress(currentIdInternal, currentPosition, duration, progress); |
|||
break; |
|||
} |
|||
}); |
|||
|
|||
// 通知原生平台启用媒体会话回调 |
|||
await _methodChannel.invokeMethod('enableMediaSessionCallbacks'); |
|||
} |
|||
|
|||
/// 更新媒体会话元数据 |
|||
Future<void> updateMediaSessionMetadata(Map<String, dynamic> song) async { |
|||
await _methodChannel.invokeMethod('updateMediaSessionMetadata', { |
|||
'song': song, |
|||
}); |
|||
} |
|||
|
|||
/// 更新媒体会话播放状态 |
|||
Future<void> updateMediaSessionPlaybackState(bool isPlaying) async { |
|||
await _methodChannel.invokeMethod('updateMediaSessionPlaybackState', { |
|||
'isPlaying': isPlaying, |
|||
}); |
|||
} |
|||
} |
|||
export 'music_service_native.dart' |
|||
if (dart.library.html) 'music_service_stub.dart'; |
|||
|
|||
@ -0,0 +1,157 @@ |
|||
import 'dart:ffi'; |
|||
|
|||
import 'package:flutter/services.dart'; |
|||
|
|||
class MusicService { |
|||
static final MusicService _instance = MusicService._internal(); |
|||
|
|||
/// 获取单例 |
|||
factory MusicService() => _instance; |
|||
|
|||
// 方法通道 |
|||
final MethodChannel _methodChannel = const MethodChannel( |
|||
'com.yunqiinnovation.music_service', |
|||
); |
|||
|
|||
// 事件通道 - 简化为只使用数据和状态通道 |
|||
late final EventChannel _dataEventChannel; |
|||
late final EventChannel _statusEventChannel; |
|||
|
|||
// 事件流 |
|||
late final Stream<dynamic> _dataStream; |
|||
late final Stream<dynamic> _statusStream; |
|||
|
|||
// 获取事件流 |
|||
Stream<dynamic> get onDataReceived => _dataStream; |
|||
|
|||
// 状态流直接传递原始数据 |
|||
Stream<dynamic> get onStatusUpdated => _statusStream; |
|||
|
|||
// 内部构造函数 |
|||
MusicService._internal() { |
|||
_dataEventChannel = const EventChannel( |
|||
'com.yunqiinnovation.music_service/data_events', |
|||
); |
|||
_statusEventChannel = const EventChannel( |
|||
'com.yunqiinnovation.music_service/status_events', |
|||
); |
|||
|
|||
// 初始化事件流 |
|||
_dataStream = _dataEventChannel.receiveBroadcastStream(); |
|||
_statusStream = _statusEventChannel.receiveBroadcastStream(); |
|||
} |
|||
|
|||
Future<void> playMusic(Map<String, dynamic> song) async { |
|||
await _methodChannel.invokeMethod('play', {'song': song}); |
|||
} |
|||
|
|||
Future<void> pauseMusic() async => _methodChannel.invokeMethod('pause'); |
|||
Future<void> resumeMusic() async => _methodChannel.invokeMethod('resume'); |
|||
Future<void> nextTrack() async => _methodChannel.invokeMethod('next'); |
|||
Future<void> previousTrack() async => _methodChannel.invokeMethod('previous'); |
|||
Future<void> stopMusic() async => _methodChannel.invokeMethod('stop'); |
|||
Future<void> setVolume(double volume) async => _methodChannel.invokeMethod( |
|||
'setVolume', |
|||
{'volume': volume}, |
|||
); // volume: 0.0~1.0 |
|||
Future<Map<String, dynamic>> getStatus() async { |
|||
final result = await _methodChannel.invokeMethod<Map>('getStatus'); |
|||
return { |
|||
'isPlaying': result?['isPlaying'] ?? false, |
|||
'id': result?['id'] ?? 0, |
|||
}; |
|||
} |
|||
|
|||
/// 获取播放进度信息 |
|||
Future<Map<String, dynamic>> getProgress() async { |
|||
final result = await _methodChannel.invokeMethod<Map>('getProgress'); |
|||
return { |
|||
'currentPosition': result?['currentPosition'] ?? 0, // 当前播放位置(毫秒) |
|||
'duration': result?['duration'] ?? 0, // 总时长(毫秒) |
|||
'progress': result?['progress'] ?? 0.0, // 播放进度(0.0-1.0) |
|||
}; |
|||
} |
|||
|
|||
/// 跳转到指定位置 |
|||
Future<void> seekTo(int positionMs) async { |
|||
await _methodChannel.invokeMethod('seekTo', {'position': positionMs}); |
|||
} |
|||
|
|||
/// 播放音乐列表 |
|||
Future<void> playMusicPlaylist(List<Map<String, dynamic>> songs) async { |
|||
await _methodChannel.invokeMethod('playlist', {'songs': songs}); |
|||
} |
|||
|
|||
/// 更新媒体会话进度信息 |
|||
Future<void> updateMediaSessionProgress( |
|||
int currentPosition, |
|||
int duration, |
|||
double progress, |
|||
) async { |
|||
await _methodChannel.invokeMethod('updateMediaSessionProgress', { |
|||
'currentPosition': currentPosition, |
|||
'duration': duration, |
|||
'progress': progress, |
|||
}); |
|||
} |
|||
|
|||
/// 设置媒体会话回调 |
|||
Future<void> setMediaSessionCallbacks({ |
|||
required Function() onPlay, |
|||
required Function() onPause, |
|||
required Function() onNext, |
|||
required Function() onPrevious, |
|||
required Function(int) onSeekTo, |
|||
required Function() onStop, |
|||
required Function(int, int, int, double) onProgress, |
|||
}) async { |
|||
// 设置方法调用处理器来处理来自原生平台的回调 |
|||
_methodChannel.setMethodCallHandler((call) async { |
|||
switch (call.method) { |
|||
case 'onMediaSessionPlay': |
|||
onPlay(); |
|||
break; |
|||
case 'onMediaSessionPause': |
|||
onPause(); |
|||
break; |
|||
case 'onMediaSessionNext': |
|||
onNext(); |
|||
break; |
|||
case 'onMediaSessionPrevious': |
|||
onPrevious(); |
|||
break; |
|||
case 'onMediaSessionSeekTo': |
|||
final position = call.arguments['position'] as int; |
|||
onSeekTo(position); |
|||
break; |
|||
case 'onMediaSessionStop': |
|||
onStop(); |
|||
break; |
|||
case 'onMediaSessionProgress': |
|||
final currentIdInternal = call.arguments['currentIdInternal'] as int; |
|||
final currentPosition = call.arguments['currentPosition'] as int; |
|||
final duration = call.arguments['duration'] as int; |
|||
final progress = call.arguments['progress'] as double; |
|||
onProgress(currentIdInternal, currentPosition, duration, progress); |
|||
break; |
|||
} |
|||
}); |
|||
|
|||
// 通知原生平台启用媒体会话回调 |
|||
await _methodChannel.invokeMethod('enableMediaSessionCallbacks'); |
|||
} |
|||
|
|||
/// 更新媒体会话元数据 |
|||
Future<void> updateMediaSessionMetadata(Map<String, dynamic> song) async { |
|||
await _methodChannel.invokeMethod('updateMediaSessionMetadata', { |
|||
'song': song, |
|||
}); |
|||
} |
|||
|
|||
/// 更新媒体会话播放状态 |
|||
Future<void> updateMediaSessionPlaybackState(bool isPlaying) async { |
|||
await _methodChannel.invokeMethod('updateMediaSessionPlaybackState', { |
|||
'isPlaying': isPlaying, |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,38 @@ |
|||
class MusicService { |
|||
static final MusicService _instance = MusicService._internal(); |
|||
factory MusicService() => _instance; |
|||
|
|||
MusicService._internal(); |
|||
|
|||
Future<void> playMusic(Map<String, dynamic> song) async {} |
|||
Future<void> playMusicPlaylist(List<Map<String, dynamic>> playlist) async {} |
|||
Future<void> pauseMusic() async {} |
|||
Future<void> resumeMusic() async {} |
|||
Future<void> stopMusic() async {} |
|||
Future<void> setVolume(double volume) async {} |
|||
Future<Map<String, dynamic>> getStatus() async => {}; |
|||
Future<Map<String, dynamic>> getProgress() async => { |
|||
'isPlaying': false, |
|||
'currentPosition': 0, |
|||
'duration': 0, |
|||
'progress': 0.0, |
|||
}; |
|||
Future<void> seekTo(int positionMs) async {} |
|||
|
|||
Future<void> updateMediaSessionProgress( |
|||
int currentPosition, int duration, double progress) async {} |
|||
|
|||
Future<void> setMediaSessionCallbacks({ |
|||
required Function() onPlay, |
|||
required Function() onPause, |
|||
required Function() onNext, |
|||
required Function() onPrevious, |
|||
required Function(int) onSeekTo, |
|||
required Function() onStop, |
|||
required Function(int, int, int, double) onProgress, |
|||
}) async {} |
|||
|
|||
Future<void> updateMediaSessionMetadata(Map<String, dynamic> song) async {} |
|||
|
|||
Future<void> updateMediaSessionPlaybackState(bool isPlaying) async {} |
|||
} |
|||
Loading…
Reference in new issue