From d31bfc4221c797af7c48a398f4667d62ddea75be Mon Sep 17 00:00:00 2001 From: hailin Date: Mon, 19 Jan 2026 23:07:35 -0800 Subject: [PATCH] =?UTF-8?q?feat(c2c):=20=E4=BF=AE=E6=94=B9C2C=E4=BA=A4?= =?UTF-8?q?=E6=98=93=E4=B8=BA=E7=A7=AF=E5=88=86=E5=80=BC=E5=B9=B6=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E7=BB=BF=E7=A7=AF=E5=88=86=E6=94=AF=E4=BB=98=E6=96=B9?= =?UTF-8?q?=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - C2C交易的商品从积分股改为积分值 - 添加绿积分作为默认且不可取消的支付方式 - 添加1.0系统ID输入框(绿积分支付必填) - 支持多种支付方式同时选择 - 更新交易提示说明 Co-Authored-By: Claude Opus 4.5 --- .../remote/trading_remote_datasource.dart | 3 + .../pages/c2c/c2c_publish_page.dart | 294 ++++++++++++------ .../presentation/providers/c2c_providers.dart | 2 + 3 files changed, 196 insertions(+), 103 deletions(-) diff --git a/frontend/mining-app/lib/data/datasources/remote/trading_remote_datasource.dart b/frontend/mining-app/lib/data/datasources/remote/trading_remote_datasource.dart index f56b9ec9..478a6b79 100644 --- a/frontend/mining-app/lib/data/datasources/remote/trading_remote_datasource.dart +++ b/frontend/mining-app/lib/data/datasources/remote/trading_remote_datasource.dart @@ -102,6 +102,7 @@ abstract class TradingRemoteDataSource { String? paymentAccount, String? paymentQrCode, String? paymentRealName, + String? systemId, // 1.0系统ID(绿积分支付必填) String? remark, }); @@ -444,6 +445,7 @@ class TradingRemoteDataSourceImpl implements TradingRemoteDataSource { String? paymentAccount, String? paymentQrCode, String? paymentRealName, + String? systemId, // 1.0系统ID(绿积分支付必填) String? remark, }) async { try { @@ -459,6 +461,7 @@ class TradingRemoteDataSourceImpl implements TradingRemoteDataSource { if (paymentAccount != null) data['paymentAccount'] = paymentAccount; if (paymentQrCode != null) data['paymentQrCode'] = paymentQrCode; if (paymentRealName != null) data['paymentRealName'] = paymentRealName; + if (systemId != null && systemId.isNotEmpty) data['systemId'] = systemId; if (remark != null && remark.isNotEmpty) data['remark'] = remark; final response = await client.post( diff --git a/frontend/mining-app/lib/presentation/pages/c2c/c2c_publish_page.dart b/frontend/mining-app/lib/presentation/pages/c2c/c2c_publish_page.dart index 69fae63c..744aa5b4 100644 --- a/frontend/mining-app/lib/presentation/pages/c2c/c2c_publish_page.dart +++ b/frontend/mining-app/lib/presentation/pages/c2c/c2c_publish_page.dart @@ -29,9 +29,10 @@ class _C2cPublishPageState extends ConsumerState { final _remarkController = TextEditingController(); // 收款信息(卖单必填) - String _paymentMethod = 'ALIPAY'; // ALIPAY, WECHAT, BANK + Set _selectedPaymentMethods = {'GREEN_POINTS'}; // 绿积分默认选中且不可取消 final _paymentAccountController = TextEditingController(); final _paymentRealNameController = TextEditingController(); + final _systemIdController = TextEditingController(); // 1.0系统ID @override void dispose() { @@ -40,6 +41,7 @@ class _C2cPublishPageState extends ConsumerState { _remarkController.dispose(); _paymentAccountController.dispose(); _paymentRealNameController.dispose(); + _systemIdController.dispose(); super.dispose(); } @@ -194,8 +196,8 @@ class _C2cPublishPageState extends ConsumerState { } Widget _buildBalanceCard(String availableShares, String availableCash) { - final isBuy = _selectedType == 0; - + // C2C交易的是积分值,买入广告需要买家有绿积分(积分值)来支付 + // 卖出广告需要卖家有积分值来出售 return Container( margin: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.all(16), @@ -206,16 +208,16 @@ class _C2cPublishPageState extends ConsumerState { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - isBuy ? '可用积分值' : '可用积分股', - style: const TextStyle(fontSize: 14, color: _grayText), + const Text( + '可用积分值', + style: TextStyle(fontSize: 14, color: _grayText), ), Text( - isBuy ? formatAmount(availableCash) : formatAmount(availableShares), - style: TextStyle( + formatAmount(availableCash), + style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, - color: isBuy ? _green : _orange, + color: _green, ), ), ], @@ -271,7 +273,7 @@ class _C2cPublishPageState extends ConsumerState { borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: _orange, width: 2), ), - suffixText: '积分值/股', + suffixText: '元/积分值', suffixStyle: const TextStyle(color: _grayText, fontSize: 14), ), onChanged: (_) => setState(() {}), @@ -286,8 +288,7 @@ class _C2cPublishPageState extends ConsumerState { String availableCash, String currentPrice, ) { - final isBuy = _selectedType == 0; - + // C2C交易的是积分值 return Container( margin: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.all(20), @@ -314,7 +315,7 @@ class _C2cPublishPageState extends ConsumerState { FilteringTextInputFormatter.allow(RegExp(r'^\d+\.?\d{0,4}')), ], decoration: InputDecoration( - hintText: '请输入数量', + hintText: '请输入积分值数量', hintStyle: const TextStyle(color: _grayText), filled: true, fillColor: _bgGray, @@ -328,17 +329,8 @@ class _C2cPublishPageState extends ConsumerState { ), suffixIcon: TextButton( onPressed: () { - if (isBuy) { - // 买入:根据可用积分值计算可买数量 - final price = double.tryParse(_priceController.text) ?? 0; - final cash = double.tryParse(availableCash) ?? 0; - if (price > 0) { - _quantityController.text = (cash / price).toStringAsFixed(4); - } - } else { - // 卖出:填入全部可用积分股 - _quantityController.text = availableShares; - } + // 填入全部可用积分值 + _quantityController.text = availableCash; setState(() {}); }, child: const Text( @@ -349,7 +341,7 @@ class _C2cPublishPageState extends ConsumerState { ), ), ), - suffixText: '积分股', + suffixText: '积分值', suffixStyle: const TextStyle(color: _grayText, fontSize: 14), ), onChanged: (_) => setState(() {}), @@ -389,33 +381,40 @@ class _C2cPublishPageState extends ConsumerState { ), const SizedBox(height: 16), - // 收款方式选择 + // 收款方式选择(多选) const Text( - '收款方式', + '收款方式(可多选)', style: TextStyle(fontSize: 14, color: _grayText), ), const SizedBox(height: 8), - Row( + Wrap( + spacing: 12, + runSpacing: 8, children: [ - _buildPaymentMethodChip('ALIPAY', '支付宝', Icons.account_balance_wallet), - const SizedBox(width: 12), - _buildPaymentMethodChip('WECHAT', '微信', Icons.chat_bubble), - const SizedBox(width: 12), - _buildPaymentMethodChip('BANK', '银行卡', Icons.credit_card), + // 绿积分 - 默认选中且不可取消 + _buildPaymentMethodCheckbox('GREEN_POINTS', '绿积分', Icons.eco, isLocked: true), + _buildPaymentMethodCheckbox('ALIPAY', '支付宝', Icons.account_balance_wallet), + _buildPaymentMethodCheckbox('WECHAT', '微信', Icons.chat_bubble), + _buildPaymentMethodCheckbox('BANK', '银行卡', Icons.credit_card), ], ), const SizedBox(height: 16), - // 收款账号 + // 1.0系统ID(绿积分必填) const Text( - '收款账号', + '1.0系统ID', style: TextStyle(fontSize: 14, color: _grayText), ), + const SizedBox(height: 4), + const Text( + '买家将通过此ID向您转账绿积分', + style: TextStyle(fontSize: 12, color: _grayText), + ), const SizedBox(height: 8), TextField( - controller: _paymentAccountController, + controller: _systemIdController, decoration: InputDecoration( - hintText: _paymentMethod == 'BANK' ? '请输入银行卡号' : '请输入收款账号', + hintText: '请输入您的1.0系统ID', hintStyle: const TextStyle(color: _grayText), filled: true, fillColor: _bgGray, @@ -429,47 +428,75 @@ class _C2cPublishPageState extends ConsumerState { ), ), ), - const SizedBox(height: 16), - // 收款人实名 - const Text( - '收款人姓名', - style: TextStyle(fontSize: 14, color: _grayText), - ), - const SizedBox(height: 8), - TextField( - controller: _paymentRealNameController, - decoration: InputDecoration( - hintText: '请输入收款人真实姓名', - hintStyle: const TextStyle(color: _grayText), - filled: true, - fillColor: _bgGray, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide.none, - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: const BorderSide(color: _orange, width: 2), + // 只有选择了非绿积分支付方式才显示收款账号和姓名 + if (_selectedPaymentMethods.any((m) => m != 'GREEN_POINTS')) ...[ + const SizedBox(height: 16), + // 收款账号 + const Text( + '收款账号(其他支付方式)', + style: TextStyle(fontSize: 14, color: _grayText), + ), + const SizedBox(height: 8), + TextField( + controller: _paymentAccountController, + decoration: InputDecoration( + hintText: '请输入收款账号', + hintStyle: const TextStyle(color: _grayText), + filled: true, + fillColor: _bgGray, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: _orange, width: 2), + ), ), ), - ), + const SizedBox(height: 16), + + // 收款人实名 + const Text( + '收款人姓名', + style: TextStyle(fontSize: 14, color: _grayText), + ), + const SizedBox(height: 8), + TextField( + controller: _paymentRealNameController, + decoration: InputDecoration( + hintText: '请输入收款人真实姓名', + hintStyle: const TextStyle(color: _grayText), + filled: true, + fillColor: _bgGray, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: _orange, width: 2), + ), + ), + ), + ], const SizedBox(height: 12), Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: _orange.withOpacity(0.1), + color: _green.withOpacity(0.1), borderRadius: BorderRadius.circular(8), ), child: Row( children: const [ - Icon(Icons.info_outline, size: 16, color: _orange), + Icon(Icons.eco, size: 16, color: _green), SizedBox(width: 8), Expanded( child: Text( - '买家会根据您填写的收款信息进行付款,请确保信息准确', - style: TextStyle(fontSize: 12, color: _orange), + '绿积分支付:买家将在1.0系统中向您的ID转账绿积分', + style: TextStyle(fontSize: 12, color: _green), ), ), ], @@ -480,14 +507,22 @@ class _C2cPublishPageState extends ConsumerState { ); } - Widget _buildPaymentMethodChip(String value, String label, IconData icon) { - final isSelected = _paymentMethod == value; + Widget _buildPaymentMethodCheckbox(String value, String label, IconData icon, {bool isLocked = false}) { + final isSelected = _selectedPaymentMethods.contains(value); return GestureDetector( - onTap: () => setState(() => _paymentMethod = value), + onTap: isLocked ? null : () { + setState(() { + if (isSelected) { + _selectedPaymentMethods.remove(value); + } else { + _selectedPaymentMethods.add(value); + } + }); + }, child: Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( - color: isSelected ? _orange : _bgGray, + color: isSelected ? (value == 'GREEN_POINTS' ? _green : _orange) : _bgGray, borderRadius: BorderRadius.circular(8), border: isSelected ? null : Border.all(color: Colors.grey.shade300), ), @@ -495,11 +530,17 @@ class _C2cPublishPageState extends ConsumerState { mainAxisSize: MainAxisSize.min, children: [ Icon( - icon, + isSelected ? Icons.check_circle : Icons.circle_outlined, size: 18, color: isSelected ? Colors.white : _grayText, ), const SizedBox(width: 6), + Icon( + icon, + size: 16, + color: isSelected ? Colors.white : _grayText, + ), + const SizedBox(width: 4), Text( label, style: TextStyle( @@ -508,6 +549,14 @@ class _C2cPublishPageState extends ConsumerState { fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, ), ), + if (isLocked) ...[ + const SizedBox(width: 4), + Icon( + Icons.lock, + size: 12, + color: isSelected ? Colors.white70 : _grayText, + ), + ], ], ), ), @@ -580,7 +629,7 @@ class _C2cPublishPageState extends ConsumerState { children: [ const Text('交易总额', style: TextStyle(fontSize: 14, color: _grayText)), Text( - '${formatAmount(total.toString())} 积分值', + '¥${formatAmount(total.toString())}', style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, @@ -590,12 +639,27 @@ class _C2cPublishPageState extends ConsumerState { ], ), const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('积分值数量', style: TextStyle(fontSize: 14, color: _grayText)), + Text( + '${formatAmount(quantity.toString())} 积分值', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: _green, + ), + ), + ], + ), + const SizedBox(height: 8), Divider(color: _orange.withOpacity(0.2)), const SizedBox(height: 8), Text( isBuy - ? '发布后,其他用户可以接单卖出积分股给您' - : '发布后,其他用户可以接单用积分值购买您的积分股', + ? '发布后,其他用户可以接单卖出积分值给您' + : '发布后,其他用户可以接单购买您的积分值', style: const TextStyle(fontSize: 12, color: _grayText), ), ], @@ -608,12 +672,17 @@ class _C2cPublishPageState extends ConsumerState { final quantity = double.tryParse(_quantityController.text) ?? 0; final isSell = _selectedType == 1; - // 卖单需要验证收款信息 + // 验证条件 bool isValid = price > 0 && quantity > 0; if (isSell) { - isValid = isValid && - _paymentAccountController.text.trim().isNotEmpty && - _paymentRealNameController.text.trim().isNotEmpty; + // 卖单必须填写1.0系统ID(因为绿积分是必选的) + isValid = isValid && _systemIdController.text.trim().isNotEmpty; + // 如果选择了其他支付方式,还需要填写收款账号和姓名 + if (_selectedPaymentMethods.any((m) => m != 'GREEN_POINTS')) { + isValid = isValid && + _paymentAccountController.text.trim().isNotEmpty && + _paymentRealNameController.text.trim().isNotEmpty; + } } return Padding( @@ -680,10 +749,11 @@ class _C2cPublishPageState extends ConsumerState { ), const SizedBox(height: 8), const Text( - '1. 发布广告后,您的资产将被冻结直到交易完成或取消\n' + '1. 发布广告后,您的积分值将被冻结直到交易完成或取消\n' '2. 其他用户接单后,需在规定时间内完成交易\n' - '3. 买方需先转账积分值,卖方确认收款后积分股自动划转\n' - '4. 如遇问题,请联系客服处理', + '3. 买方需在1.0系统中向卖方转账绿积分\n' + '4. 卖方确认收到绿积分后,积分值自动划转给买方\n' + '5. 如遇问题,请联系客服处理', style: TextStyle( fontSize: 12, color: _grayText, @@ -704,25 +774,40 @@ class _C2cPublishPageState extends ConsumerState { // 卖单验证收款信息 if (isSell) { - if (_paymentAccountController.text.trim().isEmpty) { + // 必须填写1.0系统ID(绿积分是必选的) + if (_systemIdController.text.trim().isEmpty) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('请填写收款账号'), backgroundColor: _red), + const SnackBar(content: Text('请填写1.0系统ID'), backgroundColor: _red), ); return; } - if (_paymentRealNameController.text.trim().isEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('请填写收款人姓名'), backgroundColor: _red), - ); - return; + // 如果选择了其他支付方式,需要填写收款账号和姓名 + if (_selectedPaymentMethods.any((m) => m != 'GREEN_POINTS')) { + if (_paymentAccountController.text.trim().isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('请填写收款账号'), backgroundColor: _red), + ); + return; + } + if (_paymentRealNameController.text.trim().isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('请填写收款人姓名'), backgroundColor: _red), + ); + return; + } } } - final paymentMethodText = _paymentMethod == 'ALIPAY' - ? '支付宝' - : _paymentMethod == 'WECHAT' - ? '微信' - : '银行卡'; + // 构建支付方式显示文本 + final paymentMethodTexts = _selectedPaymentMethods.map((m) { + switch (m) { + case 'GREEN_POINTS': return '绿积分'; + case 'ALIPAY': return '支付宝'; + case 'WECHAT': return '微信'; + case 'BANK': return '银行卡'; + default: return m; + } + }).join('、'); // 确认对话框 final confirmed = await showDialog( @@ -733,29 +818,31 @@ class _C2cPublishPageState extends ConsumerState { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('单价: $price 积分值/股'), + Text('单价: ¥$price/积分值'), const SizedBox(height: 8), - Text('数量: $quantity 积分股'), + Text('数量: $quantity 积分值'), const SizedBox(height: 8), Text( - '总额: ${formatAmount((double.parse(price) * double.parse(quantity)).toString())} 积分值', + '总额: ¥${formatAmount((double.parse(price) * double.parse(quantity)).toString())}', style: const TextStyle(fontWeight: FontWeight.bold), ), if (isSell) ...[ const SizedBox(height: 16), const Divider(), const SizedBox(height: 8), - Text('收款方式: $paymentMethodText'), + Text('收款方式: $paymentMethodTexts'), const SizedBox(height: 4), - Text('收款账号: ${_paymentAccountController.text.trim()}'), - const SizedBox(height: 4), - Text('收款人: ${_paymentRealNameController.text.trim()}'), + Text('1.0系统ID: ${_systemIdController.text.trim()}'), + if (_selectedPaymentMethods.any((m) => m != 'GREEN_POINTS')) ...[ + const SizedBox(height: 4), + Text('收款账号: ${_paymentAccountController.text.trim()}'), + const SizedBox(height: 4), + Text('收款人: ${_paymentRealNameController.text.trim()}'), + ], ], const SizedBox(height: 16), - Text( - _selectedType == 0 - ? '发布后,您的积分值将被冻结' - : '发布后,您的积分股将被冻结', + const Text( + '发布后,您的积分值将被冻结', style: TextStyle(fontSize: 12, color: _grayText), ), ], @@ -784,9 +871,10 @@ class _C2cPublishPageState extends ConsumerState { price: price, quantity: quantity, // 收款信息(卖单时传递) - paymentMethod: isSell ? _paymentMethod : null, + paymentMethod: isSell ? _selectedPaymentMethods.join(',') : null, paymentAccount: isSell ? _paymentAccountController.text.trim() : null, paymentRealName: isSell ? _paymentRealNameController.text.trim() : null, + systemId: isSell ? _systemIdController.text.trim() : null, remark: remark.isEmpty ? null : remark, ); diff --git a/frontend/mining-app/lib/presentation/providers/c2c_providers.dart b/frontend/mining-app/lib/presentation/providers/c2c_providers.dart index 1d422f8e..37fd7af6 100644 --- a/frontend/mining-app/lib/presentation/providers/c2c_providers.dart +++ b/frontend/mining-app/lib/presentation/providers/c2c_providers.dart @@ -87,6 +87,7 @@ class C2cTradingNotifier extends StateNotifier { String? paymentAccount, String? paymentQrCode, String? paymentRealName, + String? systemId, // 1.0系统ID(绿积分支付必填) String? remark, }) async { state = state.copyWith(isLoading: true, clearError: true); @@ -101,6 +102,7 @@ class C2cTradingNotifier extends StateNotifier { paymentAccount: paymentAccount, paymentQrCode: paymentQrCode, paymentRealName: paymentRealName, + systemId: systemId, remark: remark, ); state = state.copyWith(isLoading: false, lastOrder: order);