From 4adce2d7ed7d06c6473259d50f0d9667f9963f60 Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Thu, 6 Nov 2025 15:46:35 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E9=9F=B3=E4=B9=90=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/data/models/goods.dart | 206 +++++++++ lib/data/models/user_Info.dart | 17 +- lib/data/services/ble_manager.dart | 7 +- lib/data/services/network/api.dart | 2 +- lib/data/services/pay_service.dart | 110 +++++ .../controllers/devices_controller.dart | 2 +- .../controllers/goods_base_controller.dart | 281 ++++++++++++ .../goods/controllers/goodsai_controller.dart | 89 ++-- .../controllers/goodsmeet_controller.dart | 81 +--- .../controllers/goodstrade_controller.dart | 81 +--- .../controllers/goodsvip_controller.dart | 82 +--- lib/modules/goods/views/goodsai_view.dart | 410 +++++++++++++++++- lib/modules/goods/views/goodsmeet_view.dart | 193 ++++++++- lib/modules/goods/views/goodstrade_view.dart | 193 ++++++++- lib/modules/goods/views/goodsvip_view.dart | 196 ++++++++- .../music_service/lib/music_service.dart | 159 +------ .../lib/music_service_native.dart | 157 +++++++ .../music_service/lib/music_service_stub.dart | 38 ++ 18 files changed, 1849 insertions(+), 455 deletions(-) create mode 100644 lib/data/models/goods.dart create mode 100644 lib/modules/goods/controllers/goods_base_controller.dart create mode 100644 local_plugins/music_service/lib/music_service_native.dart create mode 100644 local_plugins/music_service/lib/music_service_stub.dart diff --git a/lib/data/models/goods.dart b/lib/data/models/goods.dart new file mode 100644 index 000000000..99485eafe --- /dev/null +++ b/lib/data/models/goods.dart @@ -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 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 toJson() { + return { + 'id': id, + 'enable': enable, + 'name': name, + 'desc': desc, + 'price': price, + 'usagetype': usagetype.toInt, + 'usagenum': usagenum, + }; + } + + /// 列表解析 + static List listFromJson(dynamic data) { + if (data == null) return const []; + if (data is List) { + return data + .map((e) => DBGoods.fromJson(e as Map)) + .toList(); + } + // 某些接口返回 { list: [...] } 或 { data: [...] } + if (data is Map) { + final list = data['list'] ?? data['data'] ?? data['items']; + if (list is List) { + return list + .map((e) => DBGoods.fromJson(e as Map)) + .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)'; + } +} diff --git a/lib/data/models/user_Info.dart b/lib/data/models/user_Info.dart index d5d369ba6..662e48d3c 100644 --- a/lib/data/models/user_Info.dart +++ b/lib/data/models/user_Info.dart @@ -41,7 +41,14 @@ class User { String phonemac; String password; int viplv = 0; //添加会员等级字段,0表示普通用户 - + // 新增的会员与用量统计字段 + int viptime = 0; // 会员到期/剩余时间(秒/毫秒,按后端定义) + int tradetotaltime = 0; // 翻译总时长 + int meettotaltime = 0; // 会议总时长 + int aichattotaltimer = 0; // 智能体总次数(兼容 aichattotaltime/aichattotaltimer) + int tradetime = 0; // 翻译剩余时长 + int meettime = 0; // 会议剩余时长 + int aichattimer = 0; // 智能体剩余次数 List devices; // 私有构造函数 @@ -125,6 +132,14 @@ class User { 'phonemac': phonemac, 'password': password, 'viplv': viplv, + 'viptime': viptime, + 'tradetotaltime': tradetotaltime, + 'meettotaltime': meettotaltime, + // 序列化统一使用 aichattotaltime 键 + 'aichattotaltime': aichattotaltimer, + 'tradetime': tradetime, + 'meettime': meettime, + 'aichattimer': aichattimer, 'devices': devices.map((device) => device.toJson()).toList(), }, }; diff --git a/lib/data/services/ble_manager.dart b/lib/data/services/ble_manager.dart index 5a5cc7d03..b53008c78 100644 --- a/lib/data/services/ble_manager.dart +++ b/lib/data/services/ble_manager.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:io'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:get_storage/get_storage.dart'; @@ -692,7 +693,11 @@ class BleManager extends GetxService { final openaiagentid = AppConfig.env('OPENAI_AGENT_ID') ?? 'personal_assistant'; final systemPrompt = AppConfig.agentsystemPrompt(openaiagentid); - final musicServiceType = AppConfig.env('APP_MUSICSERVICE_TYPE') ?? 'AUTO'; + var musicServiceType = + AppConfig.env('APP_ANDROID_MUSICSERVICE_TYPE') ?? 'AUTO'; + if (Platform.isIOS) { + musicServiceType = AppConfig.env('APP_IOS_MUSICSERVICE_TYPE') ?? 'AUTO'; + } final navigationMode = AppConfig.env('APP_NAVIGATION_MODE') ?? 'external_auto'; // 从Flutter资源包中加载.mcp.json diff --git a/lib/data/services/network/api.dart b/lib/data/services/network/api.dart index b5acf4c34..19b706bed 100644 --- a/lib/data/services/network/api.dart +++ b/lib/data/services/network/api.dart @@ -221,7 +221,7 @@ class Api { static getGoods([params]) { return DioManager().request( NWMethod.post, - '/api/home/pay_getgoods', + '/api/home/pay_getgoodss', params: params, ); } diff --git a/lib/data/services/pay_service.dart b/lib/data/services/pay_service.dart index 261feabb0..4aa34a614 100644 --- a/lib/data/services/pay_service.dart +++ b/lib/data/services/pay_service.dart @@ -1,6 +1,7 @@ import 'package:get/get.dart'; import 'package:voitrans/data/services/network/api.dart'; import 'package:tobias/tobias.dart'; +import 'package:url_launcher/url_launcher.dart'; import '../../modules/login/controllers/weixinLogin.dart'; @@ -359,4 +360,113 @@ class PayService extends GetxService { return null; } } + + /// 创建 PayPal 支付订单 + /// 返回需要打开的支付授权链接(approval_url)或支付页面URL + Future createPaypalOrder({ + required String productId, + required String productName, + String? productDesc, + required int amount, + int paymentMethod = 5, // 约定:5 表示 PayPal + String? clientIp, + String? notifyUrl, + String? extraData, + }) async { + try { + final response = await Api.paycreateorder({ + 'product_id': productId, + 'product_name': productName, + 'product_desc': productDesc ?? '', + 'amount': amount, + 'payment_method': paymentMethod, + 'client_ip': clientIp ?? '', + 'notify_url': notifyUrl ?? '', + 'extra_data': extraData ?? '', + }); + + // 兼容不同字段命名,尽量从返回中提取可用的支付URL + if (response != null) { + final candidates = [ + 'paypal_pay_url', + 'approval_url', + 'paypalApprovalUrl', + 'pay_url', + 'payment_url', + 'url', + ]; + for (final key in candidates) { + final url = response[key]; + if (url is String && url.isNotEmpty) { + return url; + } + } + + final data = response['data']; + if (data is Map) { + for (final key in candidates) { + final url = data[key]; + if (url is String && url.isNotEmpty) { + return url; + } + } + } + } + print('创建 PayPal 订单失败或未返回有效URL: $response'); + return null; + } catch (e) { + print('创建 PayPal 订单异常: $e'); + return null; + } + } + + /// 拉起 PayPal 支付(打开授权链接)。 + /// 注意:该方法仅负责跳转至 PayPal 页面,结果需要后端回调通知。 + Future paypalPay({ + required String approvalUrl, + PaymentResultCallback? onResult, + }) async { + try { + final uri = Uri.parse(approvalUrl); + final ok = await launchUrl(uri, mode: LaunchMode.externalApplication); + if (ok) { + onResult?.call(true, '已打开 PayPal,请完成支付'); + } else { + onResult?.call(false, '无法打开 PayPal 支付页面'); + } + return ok; + } catch (e) { + onResult?.call(false, '打开 PayPal 异常: $e'); + return false; + } + } + + /// 一体化的 PayPal 支付流程:创建订单 -> 打开授权链接 + Future processPaypalPayment({ + required String productId, + required int amount, + required String description, + PaymentResultCallback? onResult, + }) async { + try { + final approvalUrl = await createPaypalOrder( + productId: productId, + productName: description, + productDesc: description, + amount: amount, + paymentMethod: 5, + notifyUrl: 'https://api.ideapsound.com/api/home/pay_paypalnotify', + ); + + if (approvalUrl == null) { + onResult?.call(false, '创建 PayPal 订单失败'); + return false; + } + + return await paypalPay(approvalUrl: approvalUrl, onResult: onResult); + } catch (e) { + onResult?.call(false, 'PayPal 支付流程异常: $e'); + return false; + } + } } diff --git a/lib/modules/devices/controllers/devices_controller.dart b/lib/modules/devices/controllers/devices_controller.dart index ad2a74a73..6718af00e 100644 --- a/lib/modules/devices/controllers/devices_controller.dart +++ b/lib/modules/devices/controllers/devices_controller.dart @@ -1,4 +1,4 @@ -import 'dart:ffi'; +// import 'dart:ffi'; // Not supported on web; not needed here import 'package:voitrans/data/models/appconfig_model.dart'; diff --git a/lib/modules/goods/controllers/goods_base_controller.dart b/lib/modules/goods/controllers/goods_base_controller.dart new file mode 100644 index 000000000..243d49723 --- /dev/null +++ b/lib/modules/goods/controllers/goods_base_controller.dart @@ -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 items = [].obs; + final RxBool loading = false.obs; + final RxString error = ''.obs; + + late final PayService _payService = Get.find(); + + @override + void onInit() { + super.onInit(); + _init(); + } + + Future _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> fetchProducts(); + + /// 统一入口:不指定支付方式时,跳转到支付页面交由用户选择 + Future 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 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 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 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 } diff --git a/lib/modules/goods/controllers/goodsai_controller.dart b/lib/modules/goods/controllers/goodsai_controller.dart index c51ba35d5..fe04c175c 100644 --- a/lib/modules/goods/controllers/goodsai_controller.dart +++ b/lib/modules/goods/controllers/goodsai_controller.dart @@ -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 items = [].obs; - final RxBool loading = false.obs; - final RxString error = ''.obs; - +class GoodsaiController extends GoodsBaseController { @override - void onInit() { - super.onInit(); - _loadProducts(); - } - - Future _loadProducts() async { - loading.value = true; - error.value = ''; + Future> 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(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, - }); - } -} \ No newline at end of file + // 如需自定义跳转逻辑,可保留重写调用父类 + // Future purchase(DBGoods item) async => super.purchase(item); +} diff --git a/lib/modules/goods/controllers/goodsmeet_controller.dart b/lib/modules/goods/controllers/goodsmeet_controller.dart index 6b5c793a0..ad171e3c5 100644 --- a/lib/modules/goods/controllers/goodsmeet_controller.dart +++ b/lib/modules/goods/controllers/goodsmeet_controller.dart @@ -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 items = [].obs; - final RxBool loading = false.obs; - final RxString error = ''.obs; - +class GoodsmeetController extends GoodsBaseController { @override - void onInit() { - super.onInit(); - _loadProducts(); - } - - Future _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> 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(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, - }); - } -} \ No newline at end of file + // Future purchase(DBGoods item) async => super.purchase(item); +} diff --git a/lib/modules/goods/controllers/goodstrade_controller.dart b/lib/modules/goods/controllers/goodstrade_controller.dart index 12cf8af16..871241f9c 100644 --- a/lib/modules/goods/controllers/goodstrade_controller.dart +++ b/lib/modules/goods/controllers/goodstrade_controller.dart @@ -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 items = [].obs; - final RxBool loading = false.obs; - final RxString error = ''.obs; - +class GoodstradeController extends GoodsBaseController { @override - void onInit() { - super.onInit(); - _loadProducts(); - } - - Future _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> 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(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, - }); - } -} \ No newline at end of file + // Future purchase(DBGoods item) async => super.purchase(item); +} diff --git a/lib/modules/goods/controllers/goodsvip_controller.dart b/lib/modules/goods/controllers/goodsvip_controller.dart index 34d9dc98f..00be5dba8 100644 --- a/lib/modules/goods/controllers/goodsvip_controller.dart +++ b/lib/modules/goods/controllers/goodsvip_controller.dart @@ -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 items = [].obs; - final RxBool loading = false.obs; - final RxString error = ''.obs; - +class GoodsvipController extends GoodsBaseController { @override - void onInit() { - super.onInit(); - _loadProducts(); - } - - Future _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> 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(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, - }); - } -} \ No newline at end of file + // Future purchase(DBGoods item) async => super.purchase(item); +} diff --git a/lib/modules/goods/views/goodsai_view.dart b/lib/modules/goods/views/goodsai_view.dart index d362fedd3..fb9dc428d 100644 --- a/lib/modules/goods/views/goodsai_view.dart +++ b/lib/modules/goods/views/goodsai_view.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import '../../../data/models/goods.dart'; +import '../../../data/models/user_Info.dart'; import '../controllers/goodsai_controller.dart'; class GoodsaiView extends GetView { @@ -32,20 +34,394 @@ class GoodsaiView extends GetView { if (controller.error.isNotEmpty) { return Center(child: Text(controller.error.value)); } - return ListView.separated( + + // 用户用量信息 + int totalAi = 0; + int remainAi = 0; + if (User.isLoggedIn()) { + final u = User.instance; + totalAi = u.aichattotaltimer; + remainAi = u.aichattimer; + } + final usedAi = + (totalAi > 0) ? (totalAi - remainAi).clamp(0, totalAi) : 0; + final progress = (totalAi > 0) ? usedAi / totalAi : 0.0; + + return ListView( padding: EdgeInsets.all(16.w), - itemCount: controller.items.length, - separatorBuilder: (_, __) => SizedBox(height: 12.h), - itemBuilder: (_, index) { - final item = controller.items[index]; - return _buildCard(item, isDarkMode); - }, + children: [ + _buildHeader(isDarkMode), + SizedBox(height: 12.h), + _buildUsageCard(isDarkMode, + totalAi: totalAi, remainAi: remainAi, progress: progress), + SizedBox(height: 16.h), + Text( + '可购买套餐', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + SizedBox(height: 8.h), + ...controller.items.map((item) => Padding( + padding: EdgeInsets.only(bottom: 12.h), + child: _buildCard(item, isDarkMode), + )), + SizedBox(height: 16.h), + _buildFeaturesSection(isDarkMode), + SizedBox(height: 16.h), + _buildFooterSection(isDarkMode), + ], ); }), ); } - Widget _buildCard(AIItem item, bool isDarkMode) { + Widget _buildHeader(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( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 44.w, + height: 44.w, + decoration: BoxDecoration( + color: isDarkMode + ? Colors.indigo[900]!.withOpacity(0.3) + : Colors.indigo[100]!, + borderRadius: BorderRadius.circular(22.r), + ), + child: Icon(Icons.smart_toy, + color: isDarkMode ? Colors.indigo[300]! : Colors.indigo[600]!), + ), + SizedBox(width: 12.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'AI 功能', + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.w700, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + SizedBox(height: 6.h), + Text( + '更长对话额度、更多工具调用、更强上下文长度支持。', + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode ? Colors.white70 : Colors.grey[700], + ), + ), + SizedBox(height: 4.h), + Text( + '适用于智能体问答、总结分析、任务执行等场景。', + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode ? Colors.white70 : Colors.grey[700], + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildUsageCard( + bool isDarkMode, { + required int totalAi, + required int remainAi, + required double progress, + }) { + 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: [ + Icon(Icons.timeline, + color: isDarkMode ? Colors.blue[300]! : Colors.blue[600]!), + SizedBox(width: 8.w), + Text( + '使用进度', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + ], + ), + SizedBox(height: 12.h), + LinearProgressIndicator( + value: progress, + minHeight: 8.h, + backgroundColor: isDarkMode ? Colors.grey[800] : Colors.grey[300], + valueColor: AlwaysStoppedAnimation( + isDarkMode ? Colors.blue[300]! : Colors.blue[600]!), + ), + SizedBox(height: 8.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '总次数:$totalAi', + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode ? Colors.white70 : Colors.grey[700], + ), + ), + Text( + '剩余:$remainAi', + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode ? Colors.white70 : Colors.grey[700], + ), + ), + ], + ), + ], + ), + ); + } + + Widget _buildFeaturesSection(bool isDarkMode) { + final textColor = isDarkMode ? Colors.white : Colors.black; + final subColor = isDarkMode ? Colors.white70 : Colors.grey[700]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '功能说明', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: textColor, + ), + ), + SizedBox(height: 10.h), + Wrap( + spacing: 12.w, + runSpacing: 12.h, + alignment: WrapAlignment.start, + crossAxisAlignment: WrapCrossAlignment.start, + children: [ + _buildFeatureCard( + isDarkMode, + icon: Icons.music_note, + title: '听歌与推荐', + lines: [ + '检索曲库、播放控制', + '智能推荐歌单与相似歌曲', + ], + colorSeed: Colors.purple, + ), + _buildFeatureCard( + isDarkMode, + icon: Icons.navigation, + title: '导航与路线', + lines: [ + '路线规划与拥堵规避', + '语音播报与途径点设置', + ], + colorSeed: Colors.orange, + ), + _buildFeatureCard( + isDarkMode, + icon: Icons.wb_sunny, + title: '天气查询', + lines: [ + '实时天气与预报', + '穿衣建议与出行提醒', + ], + colorSeed: Colors.blue, + ), + _buildFeatureCard( + isDarkMode, + icon: Icons.place, + title: '附近搜索', + lines: [ + '餐饮/加油/停车等地点检索', + '评分、营业时间与路线直达', + ], + colorSeed: Colors.green, + ), + _buildFeatureCard( + isDarkMode, + icon: Icons.newspaper, + title: '新闻资讯', + lines: [ + '热点快讯与专题速览', + '长文总结与要点提炼', + ], + colorSeed: Colors.indigo, + ), + _buildFeatureCard( + isDarkMode, + icon: Icons.call, + title: '通讯助手', + lines: [ + '帮忙拨打电话与发短信', + '模板消息与日程提醒', + ], + colorSeed: Colors.teal, + ), + ], + ), + SizedBox(height: 12.h), + Text( + '以上功能受设备权限、服务可用性与网络状况影响,具体以实际表现为准。', + style: TextStyle(fontSize: 12.sp, color: subColor), + ), + ], + ); + } + + Widget _buildFooterSection(bool isDarkMode) { + final subColor = isDarkMode ? Colors.white60 : Colors.grey[600]; + return Padding( + padding: EdgeInsets.symmetric(vertical: 12.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + width: double.infinity, + child: Divider(color: isDarkMode ? Colors.white24 : Colors.black12), + ), + SizedBox(height: 8.h), + Text( + '应用名称:VoiTrans · 服务提供:VoiTrans', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 11.sp, color: subColor), + ), + SizedBox(height: 4.h), + Text( + '更多信息请前往 设置 > 关于 查看', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 11.sp, color: subColor), + ), + SizedBox(height: 4.h), + Text( + '© 2025 VoiTrans', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 11.sp, color: subColor), + ), + ], + ), + ); + } + + Widget _buildFeatureCard( + bool isDarkMode, { + required IconData icon, + required String title, + required List lines, + required MaterialColor colorSeed, + }) { + final bgColor = isDarkMode ? Colors.grey[900] : Colors.white; + final mainColor = isDarkMode ? colorSeed[300]! : colorSeed[600]!; + final subColor = isDarkMode ? Colors.white70 : Colors.grey[700]; + final width = (Get.width - 16.w * 2 - 12.w) / 2; + return Container( + width: width, + padding: EdgeInsets.all(12.w), + decoration: BoxDecoration( + color: bgColor, + 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: isDarkMode + ? mainColor.withOpacity(0.25) + : mainColor.withOpacity(0.15), + borderRadius: BorderRadius.circular(18.r), + ), + child: Icon(icon, color: mainColor), + ), + SizedBox(width: 8.w), + Expanded( + child: Text( + title, + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + SizedBox(height: 8.h), + ...lines.map((line) => Padding( + padding: EdgeInsets.only(bottom: 4.h), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(Icons.check_circle, color: mainColor, size: 14.sp), + SizedBox(width: 6.w), + Expanded( + child: Text( + line, + style: TextStyle(fontSize: 12.sp, color: subColor), + ), + ), + ], + ), + )), + ], + ), + ); + } + + Widget _buildCard(DBGoods item, bool isDarkMode) { return Container( padding: EdgeInsets.all(16.w), decoration: BoxDecoration( @@ -73,26 +449,24 @@ class GoodsaiView extends GetView { borderRadius: BorderRadius.circular(21.r), ), child: Icon(Icons.auto_awesome, - color: - isDarkMode ? Colors.teal[300]! : Colors.teal[600]!), + color: isDarkMode ? Colors.teal[300]! : Colors.teal[600]!), ), SizedBox(width: 12.w), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(item.title, + Text(item.name, style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.w600, color: isDarkMode ? Colors.white : Colors.black, )), SizedBox(height: 4.h), - Text(item.description, + Text(item.desc, style: TextStyle( fontSize: 12.sp, - color: - isDarkMode ? Colors.white70 : Colors.grey[600], + color: isDarkMode ? Colors.white70 : Colors.grey[600], )), ], ), @@ -100,7 +474,7 @@ class GoodsaiView extends GetView { Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ - Text('¥${item.price.toStringAsFixed(2)}', + Text('¥${item.priceYuan.toStringAsFixed(2)}', style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.w600, @@ -110,8 +484,8 @@ class GoodsaiView extends GetView { GestureDetector( onTap: () => controller.purchase(item), child: Container( - padding: EdgeInsets.symmetric( - horizontal: 12.w, vertical: 6.h), + padding: + EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h), decoration: BoxDecoration( color: Colors.blue[600], borderRadius: BorderRadius.circular(20.r), @@ -128,4 +502,4 @@ class GoodsaiView extends GetView { ), ); } -} \ No newline at end of file +} diff --git a/lib/modules/goods/views/goodsmeet_view.dart b/lib/modules/goods/views/goodsmeet_view.dart index 6eb214d58..25cb5e2d5 100644 --- a/lib/modules/goods/views/goodsmeet_view.dart +++ b/lib/modules/goods/views/goodsmeet_view.dart @@ -1,6 +1,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 '../controllers/goodsmeet_controller.dart'; class GoodsmeetView extends GetView { @@ -34,18 +35,24 @@ class GoodsmeetView extends GetView { } return ListView.separated( padding: EdgeInsets.all(16.w), - itemCount: controller.items.length, + itemCount: controller.items.length + 2, separatorBuilder: (_, __) => SizedBox(height: 12.h), itemBuilder: (_, index) { - final item = controller.items[index]; - return _buildCard(item, isDarkMode); + if (index < controller.items.length) { + final item = controller.items[index]; + return _buildCard(item, isDarkMode); + } + if (index == controller.items.length) { + return _buildFeaturesSection(isDarkMode); + } + return _buildFooterSection(isDarkMode); }, ); }), ); } - Widget _buildCard(MeetItem item, bool isDarkMode) { + Widget _buildCard(DBGoods item, bool isDarkMode) { return Container( padding: EdgeInsets.all(16.w), decoration: BoxDecoration( @@ -73,27 +80,24 @@ class GoodsmeetView extends GetView { borderRadius: BorderRadius.circular(21.r), ), child: Icon(Icons.people_alt, - color: isDarkMode - ? Colors.pink[300]! - : Colors.pink[600]!), + color: isDarkMode ? Colors.pink[300]! : Colors.pink[600]!), ), SizedBox(width: 12.w), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(item.title, + Text(item.name, style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.w600, color: isDarkMode ? Colors.white : Colors.black, )), SizedBox(height: 4.h), - Text(item.description, + Text(item.desc, style: TextStyle( fontSize: 12.sp, - color: - isDarkMode ? Colors.white70 : Colors.grey[600], + color: isDarkMode ? Colors.white70 : Colors.grey[600], )), ], ), @@ -111,8 +115,8 @@ class GoodsmeetView extends GetView { GestureDetector( onTap: () => controller.purchase(item), child: Container( - padding: EdgeInsets.symmetric( - horizontal: 12.w, vertical: 6.h), + padding: + EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h), decoration: BoxDecoration( color: Colors.blue[600], borderRadius: BorderRadius.circular(20.r), @@ -129,4 +133,165 @@ class GoodsmeetView extends GetView { ), ); } -} \ No newline at end of file + + Widget _buildFeaturesSection(bool isDarkMode) { + final textColor = isDarkMode ? Colors.white : Colors.black; + final subColor = isDarkMode ? Colors.white70 : Colors.grey[700]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '功能说明', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: textColor, + ), + ), + SizedBox(height: 10.h), + Wrap( + spacing: 12.w, + runSpacing: 12.h, + alignment: WrapAlignment.start, + crossAxisAlignment: WrapCrossAlignment.start, + children: [ + _buildFeatureCard( + isDarkMode, + icon: Icons.mic, + title: '会议记录', + lines: ['自动录音与分段', '讲话人标注'], + colorSeed: Colors.pink, + ), + _buildFeatureCard( + isDarkMode, + icon: Icons.summarize, + title: '转写与总结', + lines: ['长音频转写', '关键要点提炼'], + colorSeed: Colors.blue, + ), + _buildFeatureCard( + isDarkMode, + icon: Icons.search, + title: '关键词检索', + lines: ['按主题/人名检索', '时间线跳转'], + colorSeed: Colors.indigo, + ), + _buildFeatureCard( + isDarkMode, + icon: Icons.share, + title: '协同分享', + lines: ['导出纪要', '多平台分享'], + colorSeed: Colors.teal, + ), + ], + ), + SizedBox(height: 12.h), + Text( + '建议在安静环境使用,效果受麦克风与网络影响,具体以实际表现为准。', + style: TextStyle(fontSize: 12.sp, color: subColor), + ), + ], + ); + } + + Widget _buildFeatureCard( + bool isDarkMode, { + required IconData icon, + required String title, + required List lines, + required MaterialColor colorSeed, + }) { + final bgColor = isDarkMode ? Colors.grey[900] : Colors.white; + final mainColor = isDarkMode ? colorSeed[300]! : colorSeed[600]!; + final subColor = isDarkMode ? Colors.white70 : Colors.grey[700]; + final width = (Get.width - 16.w * 2 - 12.w) / 2; + return Container( + width: width, + padding: EdgeInsets.all(12.w), + decoration: BoxDecoration( + color: bgColor, + 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: isDarkMode + ? mainColor.withOpacity(0.25) + : mainColor.withOpacity(0.15), + borderRadius: BorderRadius.circular(18.r), + ), + child: Icon(icon, color: mainColor), + ), + SizedBox(width: 8.w), + Expanded( + child: Text( + title, + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + ), + ], + ), + SizedBox(height: 8.h), + ...lines.map((l) => Padding( + padding: EdgeInsets.only(bottom: 4.h), + child: + Text(l, style: TextStyle(fontSize: 12.sp, color: subColor)), + )), + ], + ), + ); + } + + Widget _buildFooterSection(bool isDarkMode) { + final subColor = isDarkMode ? Colors.white60 : Colors.grey[600]; + return Padding( + padding: EdgeInsets.symmetric(vertical: 12.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + width: double.infinity, + child: Divider(color: isDarkMode ? Colors.white24 : Colors.black12), + ), + SizedBox(height: 8.h), + Text( + '应用名称:VoiTrans · 服务提供:VoiTrans', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 11.sp, color: subColor), + ), + SizedBox(height: 4.h), + Text( + '更多信息请前往 设置 > 关于 查看', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 11.sp, color: subColor), + ), + SizedBox(height: 4.h), + Text( + '© 2025 VoiTrans', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 11.sp, color: subColor), + ), + ], + ), + ); + } +} diff --git a/lib/modules/goods/views/goodstrade_view.dart b/lib/modules/goods/views/goodstrade_view.dart index b410c03b6..8b2c619c4 100644 --- a/lib/modules/goods/views/goodstrade_view.dart +++ b/lib/modules/goods/views/goodstrade_view.dart @@ -1,6 +1,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 '../controllers/goodstrade_controller.dart'; class GoodstradeView extends GetView { @@ -34,18 +35,24 @@ class GoodstradeView extends GetView { } return ListView.separated( padding: EdgeInsets.all(16.w), - itemCount: controller.items.length, + itemCount: controller.items.length + 2, separatorBuilder: (_, __) => SizedBox(height: 12.h), itemBuilder: (_, index) { - final item = controller.items[index]; - return _buildCard(item, isDarkMode); + if (index < controller.items.length) { + final item = controller.items[index]; + return _buildCard(item, isDarkMode); + } + if (index == controller.items.length) { + return _buildFeaturesSection(isDarkMode); + } + return _buildFooterSection(isDarkMode); }, ); }), ); } - Widget _buildCard(TradeItem item, bool isDarkMode) { + Widget _buildCard(DBGoods item, bool isDarkMode) { return Container( padding: EdgeInsets.all(16.w), decoration: BoxDecoration( @@ -73,27 +80,24 @@ class GoodstradeView extends GetView { borderRadius: BorderRadius.circular(21.r), ), child: Icon(Icons.translate, - color: isDarkMode - ? Colors.indigo[300]! - : Colors.indigo[600]!), + color: isDarkMode ? Colors.indigo[300]! : Colors.indigo[600]!), ), SizedBox(width: 12.w), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(item.title, + Text(item.name, style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.w600, color: isDarkMode ? Colors.white : Colors.black, )), SizedBox(height: 4.h), - Text(item.description, + Text(item.desc, style: TextStyle( fontSize: 12.sp, - color: - isDarkMode ? Colors.white70 : Colors.grey[600], + color: isDarkMode ? Colors.white70 : Colors.grey[600], )), ], ), @@ -111,8 +115,8 @@ class GoodstradeView extends GetView { GestureDetector( onTap: () => controller.purchase(item), child: Container( - padding: EdgeInsets.symmetric( - horizontal: 12.w, vertical: 6.h), + padding: + EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h), decoration: BoxDecoration( color: Colors.blue[600], borderRadius: BorderRadius.circular(20.r), @@ -129,4 +133,165 @@ class GoodstradeView extends GetView { ), ); } -} \ No newline at end of file + + Widget _buildFeaturesSection(bool isDarkMode) { + final textColor = isDarkMode ? Colors.white : Colors.black; + final subColor = isDarkMode ? Colors.white70 : Colors.grey[700]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '功能说明', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: textColor, + ), + ), + SizedBox(height: 10.h), + Wrap( + spacing: 12.w, + runSpacing: 12.h, + alignment: WrapAlignment.start, + crossAxisAlignment: WrapCrossAlignment.start, + children: [ + _buildFeatureCard( + isDarkMode, + icon: Icons.translate, + title: '实时同传', + lines: ['中英互译', '低延时语音播报'], + colorSeed: Colors.indigo, + ), + _buildFeatureCard( + isDarkMode, + icon: Icons.text_snippet, + title: '文本翻译', + lines: ['多语种文本翻译', '术语优化与语境保持'], + colorSeed: Colors.blue, + ), + _buildFeatureCard( + isDarkMode, + icon: Icons.library_books, + title: '场景词库', + lines: ['旅游/商务/生活词库', '专属词汇记忆'], + colorSeed: Colors.teal, + ), + _buildFeatureCard( + isDarkMode, + icon: Icons.subtitles, + title: '字幕显示', + lines: ['对话字幕/大字模式', '颜色高对比度'], + colorSeed: Colors.purple, + ), + ], + ), + SizedBox(height: 12.h), + Text( + '翻译效果受麦克风、网络时延与环境噪声影响,具体以实际表现为准。', + style: TextStyle(fontSize: 12.sp, color: subColor), + ), + ], + ); + } + + Widget _buildFeatureCard( + bool isDarkMode, { + required IconData icon, + required String title, + required List lines, + required MaterialColor colorSeed, + }) { + final bgColor = isDarkMode ? Colors.grey[900] : Colors.white; + final mainColor = isDarkMode ? colorSeed[300]! : colorSeed[600]!; + final subColor = isDarkMode ? Colors.white70 : Colors.grey[700]; + final width = (Get.width - 16.w * 2 - 12.w) / 2; + return Container( + width: width, + padding: EdgeInsets.all(12.w), + decoration: BoxDecoration( + color: bgColor, + 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: isDarkMode + ? mainColor.withOpacity(0.25) + : mainColor.withOpacity(0.15), + borderRadius: BorderRadius.circular(18.r), + ), + child: Icon(icon, color: mainColor), + ), + SizedBox(width: 8.w), + Expanded( + child: Text( + title, + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + ), + ], + ), + SizedBox(height: 8.h), + ...lines.map((l) => Padding( + padding: EdgeInsets.only(bottom: 4.h), + child: + Text(l, style: TextStyle(fontSize: 12.sp, color: subColor)), + )), + ], + ), + ); + } + + Widget _buildFooterSection(bool isDarkMode) { + final subColor = isDarkMode ? Colors.white60 : Colors.grey[600]; + return Padding( + padding: EdgeInsets.symmetric(vertical: 12.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + width: double.infinity, + child: Divider(color: isDarkMode ? Colors.white24 : Colors.black12), + ), + SizedBox(height: 8.h), + Text( + '应用名称:VoiTrans · 服务提供:VoiTrans', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 11.sp, color: subColor), + ), + SizedBox(height: 4.h), + Text( + '更多信息请前往 设置 > 关于 查看', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 11.sp, color: subColor), + ), + SizedBox(height: 4.h), + Text( + '© 2025 VoiTrans', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 11.sp, color: subColor), + ), + ], + ), + ); + } +} diff --git a/lib/modules/goods/views/goodsvip_view.dart b/lib/modules/goods/views/goodsvip_view.dart index dc3067191..5a5f55896 100644 --- a/lib/modules/goods/views/goodsvip_view.dart +++ b/lib/modules/goods/views/goodsvip_view.dart @@ -1,6 +1,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 '../controllers/goodsvip_controller.dart'; class GoodsvipView extends GetView { @@ -34,19 +35,24 @@ class GoodsvipView extends GetView { } return ListView.separated( padding: EdgeInsets.all(16.w), - itemCount: controller.items.length, - separatorBuilder: (_, __) => - SizedBox(height: 12.h), + itemCount: controller.items.length + 2, + separatorBuilder: (_, __) => SizedBox(height: 12.h), itemBuilder: (_, index) { - final item = controller.items[index]; - return _buildGoodsCard(item, isDarkMode); + 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); }, ); }), ); } - Widget _buildGoodsCard(GoodsItem item, bool isDarkMode) { + Widget _buildGoodsCard(DBGoods item, bool isDarkMode) { return Container( padding: EdgeInsets.all(16.w), decoration: BoxDecoration( @@ -74,27 +80,24 @@ class GoodsvipView extends GetView { borderRadius: BorderRadius.circular(21.r), ), child: Icon(Icons.workspace_premium, - color: isDarkMode - ? Colors.amber[300]! - : Colors.amber[600]!), + color: isDarkMode ? Colors.amber[300]! : Colors.amber[600]!), ), SizedBox(width: 12.w), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(item.title, + Text(item.name, style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.w600, color: isDarkMode ? Colors.white : Colors.black, )), SizedBox(height: 4.h), - Text(item.description, + Text(item.desc, style: TextStyle( fontSize: 12.sp, - color: - isDarkMode ? Colors.white70 : Colors.grey[600], + color: isDarkMode ? Colors.white70 : Colors.grey[600], )), ], ), @@ -113,8 +116,8 @@ class GoodsvipView extends GetView { GestureDetector( onTap: () => controller.purchase(item), child: Container( - padding: EdgeInsets.symmetric( - horizontal: 12.w, vertical: 6.h), + padding: + EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h), decoration: BoxDecoration( color: Colors.blue[600], borderRadius: BorderRadius.circular(20.r), @@ -134,4 +137,165 @@ class GoodsvipView extends GetView { ), ); } -} \ No newline at end of file + + Widget _buildFeaturesSection(bool isDarkMode) { + final textColor = isDarkMode ? Colors.white : Colors.black; + final subColor = isDarkMode ? Colors.white70 : Colors.grey[700]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '功能说明', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: textColor, + ), + ), + SizedBox(height: 10.h), + Wrap( + spacing: 12.w, + runSpacing: 12.h, + alignment: WrapAlignment.start, + crossAxisAlignment: WrapCrossAlignment.start, + children: [ + _buildFeatureCard( + isDarkMode, + icon: Icons.workspace_premium, + title: '权益总览', + lines: ['更高使用额度', '更低单价'], + colorSeed: Colors.amber, + ), + _buildFeatureCard( + isDarkMode, + icon: Icons.support_agent, + title: '优先服务', + lines: ['专属客服通道', '问题快速响应'], + colorSeed: Colors.purple, + ), + _buildFeatureCard( + isDarkMode, + icon: Icons.local_offer, + title: '活动与折扣', + lines: ['会员专属活动', '长期优惠计划'], + colorSeed: Colors.indigo, + ), + _buildFeatureCard( + isDarkMode, + icon: Icons.auto_awesome, + title: '通用特权', + lines: ['新功能优先体验', '跨设备同步'], + colorSeed: Colors.teal, + ), + ], + ), + SizedBox(height: 12.h), + Text( + '具体权益以实际套餐说明为准,可能随版本迭代调整。', + style: TextStyle(fontSize: 12.sp, color: subColor), + ), + ], + ); + } + + Widget _buildFeatureCard( + bool isDarkMode, { + required IconData icon, + required String title, + required List lines, + required MaterialColor colorSeed, + }) { + final bgColor = isDarkMode ? Colors.grey[900] : Colors.white; + final mainColor = isDarkMode ? colorSeed[300]! : colorSeed[600]!; + final subColor = isDarkMode ? Colors.white70 : Colors.grey[700]; + final width = (Get.width - 16.w * 2 - 12.w) / 2; + return Container( + width: width, + padding: EdgeInsets.all(12.w), + decoration: BoxDecoration( + color: bgColor, + 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: isDarkMode + ? mainColor.withOpacity(0.25) + : mainColor.withOpacity(0.15), + borderRadius: BorderRadius.circular(18.r), + ), + child: Icon(icon, color: mainColor), + ), + SizedBox(width: 8.w), + Expanded( + child: Text( + title, + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + ), + ], + ), + SizedBox(height: 8.h), + ...lines.map((l) => Padding( + padding: EdgeInsets.only(bottom: 4.h), + child: Text(l, style: TextStyle(fontSize: 12.sp, color: subColor)), + )), + ], + ), + ); + } + + Widget _buildFooterSection(bool isDarkMode) { + final subColor = isDarkMode ? Colors.white60 : Colors.grey[600]; + return Padding( + padding: EdgeInsets.symmetric(vertical: 12.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + width: double.infinity, + child: + Divider(color: isDarkMode ? Colors.white24 : Colors.black12), + ), + SizedBox(height: 8.h), + Text( + '应用名称:VoiTrans · 服务提供:VoiTrans', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 11.sp, color: subColor), + ), + SizedBox(height: 4.h), + Text( + '更多信息请前往 设置 > 关于 查看', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 11.sp, color: subColor), + ), + SizedBox(height: 4.h), + Text( + '© 2025 VoiTrans', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 11.sp, color: subColor), + ), + ], + ), + ); + } +} diff --git a/local_plugins/music_service/lib/music_service.dart b/local_plugins/music_service/lib/music_service.dart index b7007500c..61860bbf3 100644 --- a/local_plugins/music_service/lib/music_service.dart +++ b/local_plugins/music_service/lib/music_service.dart @@ -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 _dataStream; - late final Stream _statusStream; - - // 获取事件流 - Stream get onDataReceived => _dataStream; - - // 状态流直接传递原始数据 - Stream 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 playMusic(Map song) async { - await _methodChannel.invokeMethod('play', {'song': song}); - } - - Future pauseMusic() async => _methodChannel.invokeMethod('pause'); - Future resumeMusic() async => _methodChannel.invokeMethod('resume'); - Future nextTrack() async => _methodChannel.invokeMethod('next'); - Future previousTrack() async => _methodChannel.invokeMethod('previous'); - Future stopMusic() async => _methodChannel.invokeMethod('stop'); - Future setVolume(double volume) async => _methodChannel.invokeMethod( - 'setVolume', - {'volume': volume}, - ); // volume: 0.0~1.0 - Future> getStatus() async { - final result = await _methodChannel.invokeMethod('getStatus'); - return { - 'isPlaying': result?['isPlaying'] ?? false, - 'id': result?['id'] ?? 0, - }; - } - - /// 获取播放进度信息 - Future> getProgress() async { - final result = await _methodChannel.invokeMethod('getProgress'); - return { - 'currentPosition': result?['currentPosition'] ?? 0, // 当前播放位置(毫秒) - 'duration': result?['duration'] ?? 0, // 总时长(毫秒) - 'progress': result?['progress'] ?? 0.0, // 播放进度(0.0-1.0) - }; - } - - /// 跳转到指定位置 - Future seekTo(int positionMs) async { - await _methodChannel.invokeMethod('seekTo', {'position': positionMs}); - } - - /// 播放音乐列表 - Future playMusicPlaylist(List> songs) async { - await _methodChannel.invokeMethod('playlist', {'songs': songs}); - } - - /// 更新媒体会话进度信息 - Future updateMediaSessionProgress( - int currentPosition, - int duration, - double progress, - ) async { - await _methodChannel.invokeMethod('updateMediaSessionProgress', { - 'currentPosition': currentPosition, - 'duration': duration, - 'progress': progress, - }); - } - - /// 设置媒体会话回调 - Future 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 updateMediaSessionMetadata(Map song) async { - await _methodChannel.invokeMethod('updateMediaSessionMetadata', { - 'song': song, - }); - } - - /// 更新媒体会话播放状态 - Future updateMediaSessionPlaybackState(bool isPlaying) async { - await _methodChannel.invokeMethod('updateMediaSessionPlaybackState', { - 'isPlaying': isPlaying, - }); - } -} +export 'music_service_native.dart' + if (dart.library.html) 'music_service_stub.dart'; diff --git a/local_plugins/music_service/lib/music_service_native.dart b/local_plugins/music_service/lib/music_service_native.dart new file mode 100644 index 000000000..b7007500c --- /dev/null +++ b/local_plugins/music_service/lib/music_service_native.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 _dataStream; + late final Stream _statusStream; + + // 获取事件流 + Stream get onDataReceived => _dataStream; + + // 状态流直接传递原始数据 + Stream 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 playMusic(Map song) async { + await _methodChannel.invokeMethod('play', {'song': song}); + } + + Future pauseMusic() async => _methodChannel.invokeMethod('pause'); + Future resumeMusic() async => _methodChannel.invokeMethod('resume'); + Future nextTrack() async => _methodChannel.invokeMethod('next'); + Future previousTrack() async => _methodChannel.invokeMethod('previous'); + Future stopMusic() async => _methodChannel.invokeMethod('stop'); + Future setVolume(double volume) async => _methodChannel.invokeMethod( + 'setVolume', + {'volume': volume}, + ); // volume: 0.0~1.0 + Future> getStatus() async { + final result = await _methodChannel.invokeMethod('getStatus'); + return { + 'isPlaying': result?['isPlaying'] ?? false, + 'id': result?['id'] ?? 0, + }; + } + + /// 获取播放进度信息 + Future> getProgress() async { + final result = await _methodChannel.invokeMethod('getProgress'); + return { + 'currentPosition': result?['currentPosition'] ?? 0, // 当前播放位置(毫秒) + 'duration': result?['duration'] ?? 0, // 总时长(毫秒) + 'progress': result?['progress'] ?? 0.0, // 播放进度(0.0-1.0) + }; + } + + /// 跳转到指定位置 + Future seekTo(int positionMs) async { + await _methodChannel.invokeMethod('seekTo', {'position': positionMs}); + } + + /// 播放音乐列表 + Future playMusicPlaylist(List> songs) async { + await _methodChannel.invokeMethod('playlist', {'songs': songs}); + } + + /// 更新媒体会话进度信息 + Future updateMediaSessionProgress( + int currentPosition, + int duration, + double progress, + ) async { + await _methodChannel.invokeMethod('updateMediaSessionProgress', { + 'currentPosition': currentPosition, + 'duration': duration, + 'progress': progress, + }); + } + + /// 设置媒体会话回调 + Future 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 updateMediaSessionMetadata(Map song) async { + await _methodChannel.invokeMethod('updateMediaSessionMetadata', { + 'song': song, + }); + } + + /// 更新媒体会话播放状态 + Future updateMediaSessionPlaybackState(bool isPlaying) async { + await _methodChannel.invokeMethod('updateMediaSessionPlaybackState', { + 'isPlaying': isPlaying, + }); + } +} diff --git a/local_plugins/music_service/lib/music_service_stub.dart b/local_plugins/music_service/lib/music_service_stub.dart new file mode 100644 index 000000000..0393682dd --- /dev/null +++ b/local_plugins/music_service/lib/music_service_stub.dart @@ -0,0 +1,38 @@ +class MusicService { + static final MusicService _instance = MusicService._internal(); + factory MusicService() => _instance; + + MusicService._internal(); + + Future playMusic(Map song) async {} + Future playMusicPlaylist(List> playlist) async {} + Future pauseMusic() async {} + Future resumeMusic() async {} + Future stopMusic() async {} + Future setVolume(double volume) async {} + Future> getStatus() async => {}; + Future> getProgress() async => { + 'isPlaying': false, + 'currentPosition': 0, + 'duration': 0, + 'progress': 0.0, + }; + Future seekTo(int positionMs) async {} + + Future updateMediaSessionProgress( + int currentPosition, int duration, double progress) async {} + + Future 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 updateMediaSessionMetadata(Map song) async {} + + Future updateMediaSessionPlaybackState(bool isPlaying) async {} +}