import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:tdesign_flutter/tdesign_flutter.dart'; import '../../../core/i18n/app_localizations.dart'; import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_colors_extension.dart'; import '../../../core/data/mock_api_data.dart'; /// 报销明细输入数据。 class ExpenseDetailInputData { final String category; final String categoryName; final String purpose; final double amount; // 含税金额 final double taxRate; final String customerVendorName; final double offsetAmount; final String remark; const ExpenseDetailInputData({ required this.category, required this.categoryName, required this.purpose, required this.amount, required this.taxRate, this.customerVendorName = '', this.offsetAmount = 0.0, this.remark = '', }); } /// 报销明细编辑弹窗。 /// /// 使用 [TDSlidePopupRoute] 从底部滑出,卡片化展示表单字段。 /// 参照 ExpenseApplyCreatePage 的 ExpenseDetailDialog 样式。 class ExpenseDetailDialog extends StatefulWidget { final List categories; final AppLocalizations l10n; const ExpenseDetailDialog({ super.key, required this.categories, required this.l10n, }); /// 显示弹窗,返回 [ExpenseDetailInputData] 或 `null`(取消时)。 static Future show( BuildContext context, { required List categories, required AppLocalizations l10n, }) { FocusScope.of(context).unfocus(); return Navigator.push( context, TDSlidePopupRoute( slideTransitionFrom: SlideTransitionFrom.bottom, isDismissible: false, builder: (_) => ExpenseDetailDialog( categories: categories, l10n: l10n, ), ), ); } @override State createState() => _ExpenseDetailDialogState(); } class _ExpenseDetailDialogState extends State { late String _cat; late String _catLabel; late TextEditingController _descCtrl; late TextEditingController _amountCtrl; late TextEditingController _customerCtrl; late TextEditingController _offsetCtrl; late TextEditingController _remarkCtrl; double _taxRate = 0.06; static const _taxOptions = [0.06, 0.09, 0.13]; static const _taxLabels = ['6%', '9%', '13%']; List get _cats => widget.categories; AppLocalizations get _l10n => widget.l10n; @override void initState() { super.initState(); _cat = _cats.isNotEmpty ? _cats.first.code : 'other'; _catLabel = _l10n.get(_cats.firstWhere((c) => c.code == _cat).nameKey); _descCtrl = TextEditingController(); _amountCtrl = TextEditingController(); _customerCtrl = TextEditingController(); _offsetCtrl = TextEditingController(); _remarkCtrl = TextEditingController(); } @override void dispose() { _descCtrl.dispose(); _amountCtrl.dispose(); _customerCtrl.dispose(); _offsetCtrl.dispose(); _remarkCtrl.dispose(); super.dispose(); } void _confirm() { final amount = double.tryParse(_amountCtrl.text) ?? 0; final desc = _descCtrl.text.trim(); if (desc.isEmpty) { TDToast.showText(_l10n.get('enterExpenseName'), context: context); return; } if (amount <= 0) { TDToast.showText(_l10n.get('amountPositive'), context: context); return; } Navigator.pop( context, ExpenseDetailInputData( category: _cat, categoryName: _l10n.get( _cats.firstWhere((c) => c.code == _cat).nameKey, ), purpose: desc, amount: amount, taxRate: _taxRate, customerVendorName: _customerCtrl.text.trim(), offsetAmount: double.tryParse(_offsetCtrl.text) ?? 0, remark: _remarkCtrl.text.trim(), ), ); } double get _amountExclTax => _taxRate > 0 ? (double.tryParse(_amountCtrl.text) ?? 0) / (1 + _taxRate) : (double.tryParse(_amountCtrl.text) ?? 0); double get _taxAmount => (double.tryParse(_amountCtrl.text) ?? 0) - _amountExclTax; @override Widget build(BuildContext context) { final colors = Theme.of(context).extension()!; final bottomInset = MediaQuery.of(context).viewInsets.bottom; return SafeArea( child: Padding( padding: EdgeInsets.only(bottom: bottomInset), child: Container( decoration: BoxDecoration( color: colors.bgPage, borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _buildHeader(colors), Flexible( child: SingleChildScrollView( padding: const EdgeInsets.fromLTRB(16, 0, 16, 12), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _buildCategoryCard(colors), const SizedBox(height: 12), _buildDescCard(), const SizedBox(height: 12), _buildAmountCard(), const SizedBox(height: 12), _buildTaxRateCard(colors), const SizedBox(height: 12), // 自动计算展示 _buildCalcInfo(colors), const SizedBox(height: 12), _buildCustomerCard(), const SizedBox(height: 12), _buildOffsetCard(), const SizedBox(height: 12), _buildRemarkCard(colors), ], ), ), ), Container( padding: const EdgeInsets.fromLTRB(16, 12, 16, 16), decoration: BoxDecoration( color: colors.bgCard, border: Border( top: BorderSide(color: colors.border, width: 0.5), ), ), child: _buildActions(), ), ], ), ), ), ); } // ── 标题栏 ── Widget _buildHeader(AppColorsExtension colors) { return Column( mainAxisSize: MainAxisSize.min, children: [ Center( child: Container( margin: const EdgeInsets.only(top: 8, bottom: 4), width: 36, height: 4, decoration: BoxDecoration( color: colors.border, borderRadius: BorderRadius.circular(2), ), ), ), Padding( padding: const EdgeInsets.fromLTRB(20, 8, 12, 16), child: Row( children: [ const SizedBox(width: 28), Expanded( child: Center( child: Text( _l10n.get('addExpenseDetail'), style: TextStyle( fontSize: AppFontSizes.title, fontWeight: FontWeight.w600, color: colors.textPrimary, ), ), ), ), GestureDetector( onTap: () => Navigator.pop(context), child: Padding( padding: const EdgeInsets.all(4), child: Icon( Icons.close, size: 20, color: colors.textSecondary, ), ), ), ], ), ), ], ); } // ── picker 卡片 ── Widget _pickerCard({ required String label, required bool required, required String currentLabel, required List labels, required ValueChanged onSelected, required AppColorsExtension colors, }) { final tdTheme = TDTheme.of(context); return GestureDetector( onTap: () { TDPicker.showMultiPicker( context, title: label, backgroundColor: colors.bgCard, data: [labels], onConfirm: (selected) { if (selected.isNotEmpty && selected[0] is int) { final idx = selected[0] as int; if (idx >= 0 && idx < labels.length) { Navigator.of(context).pop(); onSelected(idx); } } }, ); }, child: Container( padding: const EdgeInsets.only( left: 16, right: 16, top: 12, bottom: 12, ), decoration: BoxDecoration( color: tdTheme.bgColorContainer, borderRadius: BorderRadius.circular(tdTheme.radiusDefault), border: Border.all(color: tdTheme.componentStrokeColor), ), child: Row( children: [ TDText( label, maxLines: 1, overflow: TextOverflow.visible, font: tdTheme.fontBodyLarge, fontWeight: FontWeight.w400, style: const TextStyle(letterSpacing: 0), ), if (required) Padding( padding: const EdgeInsets.only(left: 4), child: TDText( '*', font: tdTheme.fontBodyLarge, fontWeight: FontWeight.w400, style: TextStyle(color: tdTheme.errorColor6), ), ), const SizedBox(width: 12), Expanded( child: Row( mainAxisAlignment: MainAxisAlignment.end, mainAxisSize: MainAxisSize.max, children: [ Flexible( child: TDText( currentLabel, maxLines: 1, overflow: TextOverflow.ellipsis, font: tdTheme.fontBodyLarge, fontWeight: FontWeight.w400, textColor: tdTheme.textColorPrimary, textAlign: TextAlign.end, ), ), const SizedBox(width: 4), Icon( Icons.chevron_right, size: 18, color: tdTheme.textColorPlaceholder, ), ], ), ), ], ), ), ); } // ── 费用类别 ── Widget _buildCategoryCard(AppColorsExtension colors) { return _pickerCard( label: _l10n.get('expenseCategory'), required: true, currentLabel: _catLabel, labels: _cats.map((c) => _l10n.get(c.nameKey)).toList(), colors: colors, onSelected: (idx) => setState(() { _cat = _cats[idx].code; _catLabel = _l10n.get(_cats[idx].nameKey); }), ); } // ── 费用项目 ── Widget _buildDescCard() { final screenWidth = MediaQuery.of(context).size.width; return TDInput( type: TDInputType.cardStyle, cardStyle: TDCardStyle.topText, width: screenWidth - 32, leftLabel: _l10n.get('expenseName'), required: true, controller: _descCtrl, hintText: _l10n.get('enterExpenseName'), contentAlignment: TextAlign.center, showBottomDivider: false, onChanged: (_) => setState(() {}), onClearTap: () { _descCtrl.clear(); setState(() {}); }, ); } // ── 金额(含税) ── Widget _buildAmountCard() { final screenWidth = MediaQuery.of(context).size.width; return TDInput( type: TDInputType.cardStyle, cardStyle: TDCardStyle.topText, width: screenWidth - 32, leftLabel: _l10n.get('amountInclTax'), required: true, controller: _amountCtrl, hintText: '>0', contentAlignment: TextAlign.center, inputType: const TextInputType.numberWithOptions(decimal: true), inputFormatters: [ FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}$')), ], showBottomDivider: false, onChanged: (_) => setState(() {}), onClearTap: () { _amountCtrl.clear(); setState(() {}); }, ); } // ── 税率 ── Widget _buildTaxRateCard(AppColorsExtension colors) { final currentLabel = '${(_taxRate * 100).toStringAsFixed(0)}%'; return _pickerCard( label: _l10n.get('taxRate'), required: true, currentLabel: currentLabel, labels: _taxLabels.toList(), colors: colors, onSelected: (idx) => setState(() { _taxRate = _taxOptions[idx]; }), ); } // ── 计算信息 ── Widget _buildCalcInfo(AppColorsExtension colors) { final amount = double.tryParse(_amountCtrl.text) ?? 0; if (amount <= 0) return const SizedBox.shrink(); return Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), decoration: BoxDecoration( color: colors.primary50, borderRadius: BorderRadius.circular(8), ), child: Row( children: [ Expanded( child: Text( '${_l10n.get('amountExcludingTax')}: ¥${_amountExclTax.toStringAsFixed(2)}', style: TextStyle( fontSize: AppFontSizes.body, color: colors.textSecondary, ), ), ), Text( '${_l10n.get('taxAmount')}: ¥${_taxAmount.toStringAsFixed(2)}', style: TextStyle( fontSize: AppFontSizes.body, color: colors.textSecondary, ), ), ], ), ); } // ── 客户/厂商 ── Widget _buildCustomerCard() { final screenWidth = MediaQuery.of(context).size.width; return TDInput( type: TDInputType.cardStyle, cardStyle: TDCardStyle.topText, width: screenWidth - 32, leftLabel: _l10n.get('customerVendor'), controller: _customerCtrl, hintText: _l10n.get('optional'), contentAlignment: TextAlign.center, showBottomDivider: false, onChanged: (_) => setState(() {}), onClearTap: () { _customerCtrl.clear(); setState(() {}); }, ); } // ── 已充金额 ── Widget _buildOffsetCard() { final screenWidth = MediaQuery.of(context).size.width; return TDInput( type: TDInputType.cardStyle, cardStyle: TDCardStyle.topText, width: screenWidth - 32, leftLabel: _l10n.get('offsetAmount'), controller: _offsetCtrl, hintText: '0', contentAlignment: TextAlign.center, inputType: const TextInputType.numberWithOptions(decimal: true), inputFormatters: [ FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}$')), ], showBottomDivider: false, onChanged: (_) => setState(() {}), onClearTap: () { _offsetCtrl.clear(); setState(() {}); }, ); } // ── 备注 ── Widget _buildRemarkCard(AppColorsExtension colors) { final tdTheme = TDTheme.of(context); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.only(left: 4), child: TDText( _l10n.get('detailRemark'), font: tdTheme.fontBodyLarge, fontWeight: FontWeight.w400, style: const TextStyle(letterSpacing: 0), ), ), const SizedBox(height: 8), TDTextarea( controller: _remarkCtrl, hintText: _l10n.get('optional'), maxLines: 3, minLines: 1, maxLength: 200, indicator: true, padding: EdgeInsets.zero, bordered: true, inputType: TextInputType.multiline, backgroundColor: tdTheme.bgColorContainer, ), ], ); } // ── 操作按钮 ── Widget _buildActions() { return Row( children: [ Expanded( child: TDButton( text: _l10n.get('cancel'), size: TDButtonSize.large, type: TDButtonType.outline, shape: TDButtonShape.rectangle, theme: TDButtonTheme.defaultTheme, onTap: () => Navigator.pop(context), ), ), const SizedBox(width: 12), Expanded( child: TDButton( text: _l10n.get('confirmAdd'), size: TDButtonSize.large, type: TDButtonType.fill, shape: TDButtonShape.rectangle, theme: TDButtonTheme.primary, onTap: _confirm, ), ), ], ); } }