| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937 |
- import 'package:flutter/material.dart';
- import 'package:flutter_riverpod/flutter_riverpod.dart';
- import 'package:tdesign_flutter/tdesign_flutter.dart';
- import 'package:go_router/go_router.dart';
- import '../../shared/widgets/nav_bar_config.dart';
- import '../../core/utils/responsive.dart';
- import '../../shared/widgets/form_section.dart';
- import '../../shared/widgets/form_field_row.dart';
- import 'expense_apply_controller.dart';
- import '../../core/i18n/app_localizations.dart';
- import 'expense_model.dart';
- import '../../core/theme/app_colors.dart';
- import '../../core/theme/app_colors_extension.dart';
- import '../../core/data/mock_api_data.dart';
- import 'widgets/expense_detail_edit_dialog.dart';
- class ExpenseApplyPage extends ConsumerStatefulWidget {
- final String? editId;
- const ExpenseApplyPage({super.key, this.editId});
- @override
- ConsumerState<ExpenseApplyPage> createState() => _ExpenseApplyPageState();
- }
- class _ExpenseApplyPageState extends ConsumerState<ExpenseApplyPage> {
- final _remarkController = TextEditingController();
- final _purposeController = TextEditingController();
- final _voucherNoController = TextEditingController();
- final _bankNameController = TextEditingController(text: '中国银行');
- final _accountNameController = TextEditingController(text: '张三');
- // 关联管控
- String? _selectedProjectName;
- String? _selectedSubjectName;
- double _availableBudget = 0;
- @override
- void dispose() {
- _remarkController.dispose();
- _purposeController.dispose();
- _voucherNoController.dispose();
- _bankNameController.dispose();
- _accountNameController.dispose();
- super.dispose();
- }
- @override
- Widget build(BuildContext context) {
- final controller = ref.watch(expenseApplyProvider(widget.editId).notifier);
- final state = ref.watch(expenseApplyProvider(widget.editId));
- final r = ResponsiveHelper.of(context);
- final l10n = AppLocalizations.of(context);
- ref
- .read(navBarConfigProvider.notifier)
- .update(
- NavBarConfig(
- title: widget.editId != null
- ? l10n.get('editExpense')
- : l10n.get('expenseApply'),
- showBack: true,
- onBack: () => context.pop(),
- ),
- );
- final colors = Theme.of(context).extension<AppColorsExtension>()!;
- final bottomInset = MediaQuery.of(context).padding.bottom;
- return Column(
- children: [
- Expanded(
- child: Align(
- alignment: Alignment.topCenter,
- child: ConstrainedBox(
- constraints: BoxConstraints(maxWidth: r.formMaxWidth),
- child: SingleChildScrollView(
- padding: const EdgeInsets.all(16),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- // 1. 导入事前申请链接
- _buildImportLink(),
- const SizedBox(height: 16),
- // 2. 基本信息区
- _buildBasicInfoSection(controller, state),
- const SizedBox(height: 16),
- // 3. 关联管控区
- _buildControlSection(controller, state),
- const SizedBox(height: 16),
- // 4. 收款账户区
- _buildAccountSection(controller, state),
- const SizedBox(height: 16),
- // 5. 报销明细区
- _buildDetailSection(controller, state),
- const SizedBox(height: 16),
- // 6. 附件上传区
- _buildInvoiceSection(controller, state),
- ],
- ),
- ),
- ),
- ),
- ),
- // 7. 底部操作栏 + iOS 横条指示器背景
- ColoredBox(
- color: colors.bgCard,
- child: Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- _buildBottomButtons(controller, state),
- if (bottomInset > 0) SizedBox(height: bottomInset),
- ],
- ),
- ),
- ],
- );
- }
- // ═══ 1. 导入事前申请链接 ═══
- Widget _buildImportLink() {
- final colors = Theme.of(context).extension<AppColorsExtension>()!;
- final l10n = AppLocalizations.of(context);
- return GestureDetector(
- onTap: () {
- TDToast.showText(l10n.get('expenseApplyImport'), context: context);
- },
- child: Container(
- height: 44,
- decoration: BoxDecoration(
- color: colors.primaryLight,
- borderRadius: BorderRadius.circular(8),
- ),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- Icon(Icons.download, size: 14, color: colors.primary),
- const SizedBox(width: 8),
- Text(
- l10n.get('importApprovedPreApp'),
- style: TextStyle(
- fontSize: AppFontSizes.body,
- color: colors.primary,
- ),
- ),
- ],
- ),
- ),
- );
- }
- // ═══ 2. 基本信息区 ═══
- // 报销日期(只读)、报销单号(只读)、币别(TDPicker)、报销人员(只读)、报销部门(只读)、
- // 支付方式(TDPicker)、凭证号码、备注
- Widget _buildBasicInfoSection(
- ExpenseApplyController controller,
- ExpenseApplyState state,
- ) {
- final l10n = AppLocalizations.of(context);
- final expense = state.expense;
- return FormSection(
- title: l10n.get('basicInfo'),
- leadingIcon: Icons.info_outline,
- children: [
- // 报销日期(只读)
- FormFieldRow(
- label: l10n.get('expenseDate'),
- value: expense.applicationDate != null
- ? '${expense.applicationDate!.year}-${expense.applicationDate!.month.toString().padLeft(2, '0')}-${expense.applicationDate!.day.toString().padLeft(2, '0')}'
- : _today(),
- readOnly: true,
- showArrow: false,
- ),
- const SizedBox(height: 16),
- // 报销单号(只读)
- FormFieldRow(
- label: l10n.get('reportNo'),
- value: expense.reportNo.isNotEmpty ? expense.reportNo : null,
- hint: l10n.get('autoGenerated'),
- readOnly: true,
- showArrow: false,
- ),
- const SizedBox(height: 16),
- // 币别(TDPicker)
- FormFieldRow(
- label: l10n.get('currency'),
- value: expense.currencyCode.isNotEmpty ? expense.currencyCode : 'CNY',
- hint: l10n.get('selectCurrency'),
- onTap: () => _showCurrencyPicker(controller, expense.currencyCode),
- ),
- const SizedBox(height: 16),
- // 报销人员(只读)
- FormFieldRow(
- label: l10n.get('applicant'),
- value: expense.applicantName.isNotEmpty
- ? expense.applicantName
- : '张三',
- readOnly: true,
- showArrow: false,
- ),
- const SizedBox(height: 16),
- // 报销部门(只读)
- FormFieldRow(
- label: l10n.get('department'),
- value: expense.deptName.isNotEmpty ? expense.deptName : '技术部',
- readOnly: true,
- showArrow: false,
- ),
- const SizedBox(height: 16),
- // 支付方式(TDPicker)
- FormFieldRow(
- label: l10n.get('paymentMethod'),
- value: expense.paymentMethod.isNotEmpty
- ? expense.paymentMethod
- : null,
- hint: l10n.get('selectPaymentMethod'),
- onTap: () => _showPaymentMethodPicker(controller),
- ),
- const SizedBox(height: 16),
- // 凭证号码
- FormFieldRow(
- label: l10n.get('voucherNo'),
- value: _voucherNoController.text.isNotEmpty
- ? _voucherNoController.text
- : null,
- hint: l10n.get('enterVoucherNo'),
- onTap: () => _showTextInput(
- l10n.get('voucherNo'),
- l10n.get('enterVoucherNo'),
- (v) => setState(() {
- _voucherNoController.text = v;
- _voucherNoController.selection = TextSelection.fromPosition(
- TextPosition(offset: v.length),
- );
- }),
- initialText: _voucherNoController.text,
- ),
- ),
- const SizedBox(height: 16),
- // 备注
- FormFieldRow(
- label: l10n.get('remark'),
- value: _remarkController.text.isNotEmpty
- ? _remarkController.text
- : null,
- hint: l10n.get('enterRemark'),
- onTap: () => _showTextInput(
- l10n.get('remark'),
- l10n.get('enterRemark'),
- (v) => setState(() {
- _remarkController.text = v;
- _remarkController.selection = TextSelection.fromPosition(
- TextPosition(offset: v.length),
- );
- }),
- initialText: _remarkController.text,
- ),
- ),
- ],
- );
- }
- // ═══ 3. 关联管控区 ═══
- // 报销事由、关联项目&预算科目(级联)、成本中心、可用预算、报销金额合计(只读)、核准金额合计(只读)
- Widget _buildControlSection(
- ExpenseApplyController controller,
- ExpenseApplyState state,
- ) {
- final colors = Theme.of(context).extension<AppColorsExtension>()!;
- final l10n = AppLocalizations.of(context);
- final expense = state.expense;
- final cascadeLabel = _selectedProjectName != null &&
- _selectedSubjectName != null
- ? '$_selectedProjectName / $_selectedSubjectName'
- : _selectedProjectName;
- return FormSection(
- title: l10n.get('relatedControl'),
- leadingIcon: Icons.link_outlined,
- children: [
- // 报销事由
- FormFieldRow(
- label: l10n.get('expenseReason'),
- value: _purposeController.text.isNotEmpty
- ? _purposeController.text
- : null,
- hint: l10n.get('enterExpenseReason'),
- onTap: () => _showTextInput(
- l10n.get('expenseReason'),
- l10n.get('enterExpenseReason'),
- (v) {
- setState(() {
- _purposeController.text = v;
- _purposeController.selection = TextSelection.fromPosition(
- TextPosition(offset: v.length),
- );
- });
- controller.updatePurpose(v);
- },
- initialText: _purposeController.text,
- ),
- ),
- const SizedBox(height: 16),
- // 关联项目 & 预算科目(级联选择器,参考 ExpenseApplicationApplyPage)
- FormFieldRow(
- label: l10n.get('relatedProject'),
- value: cascadeLabel,
- hint: l10n.get('selectProjectAndSubject'),
- required: true,
- onTap: () {
- FocusScope.of(context).unfocus();
- TDCascader.showMultiCascader(
- context,
- title: l10n.get('selectProjectAndSubject'),
- data: _buildCascadeData(),
- subTitles: [
- l10n.get('project'),
- l10n.get('budgetSubject'),
- ],
- onClose: () => Navigator.of(context).pop(),
- onChange: (selected) {
- if (selected.length >= 2) {
- final pId = int.tryParse(selected[0].value ?? '');
- final sId = int.tryParse(selected[1].value ?? '');
- if (pId != null && sId != null) {
- setState(() {
- _selectedProjectName = selected[0].label;
- _selectedSubjectName = selected[1].label;
- _availableBudget = getMockBudget(pId, sId);
- });
- }
- }
- },
- );
- },
- ),
- const SizedBox(height: 16),
- // 可用预算
- _buildBudgetRow(l10n, colors),
- const SizedBox(height: 16),
- // 成本中心
- FormFieldRow(
- label: l10n.get('costCenter'),
- value: expense.costCenterId.isNotEmpty ? expense.costCenterId : null,
- hint: l10n.get('selectCostCenter'),
- onTap: () {
- TDToast.showText(l10n.get('costCenterSelection'), context: context);
- },
- ),
- ],
- );
- }
- List<Map<String, dynamic>> _buildCascadeData() {
- return mockProjects
- .map(
- (p) => <String, dynamic>{
- 'label': p.name,
- 'value': p.id.toString(),
- 'children': mockBudgetSubjects
- .map(
- (s) => <String, dynamic>{
- 'label': s.name,
- 'value': s.id.toString(),
- },
- )
- .toList(),
- },
- )
- .toList();
- }
- Widget _buildBudgetRow(AppLocalizations l10n, AppColorsExtension colors) {
- final over = _availableBudget <= 0;
- return Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- Text(
- l10n.get('availableBudget'),
- style: TextStyle(
- fontSize: AppFontSizes.subtitle,
- color: colors.textSecondary,
- ),
- ),
- Text(
- '¥${_availableBudget.toStringAsFixed(2)}',
- style: TextStyle(
- fontSize: AppFontSizes.subtitle,
- fontWeight: FontWeight.w700,
- color: over ? colors.danger : colors.amountPrimary,
- ),
- ),
- ],
- );
- }
- // ═══ 4. 收款账户区 ═══
- // 开户行(bankName)、户名(accountName)、银行账号(bankAccount)
- Widget _buildAccountSection(
- ExpenseApplyController controller,
- ExpenseApplyState state,
- ) {
- final l10n = AppLocalizations.of(context);
- return FormSection(
- title: l10n.get('receiptAccount'),
- leadingIcon: Icons.account_balance_outlined,
- children: [
- FormFieldRow(
- label: l10n.get('bankName'),
- value: _bankNameController.text.isNotEmpty
- ? _bankNameController.text
- : null,
- hint: l10n.get('selectBank'),
- onTap: () {
- TDToast.showText(l10n.get('bankSelection'), context: context);
- },
- ),
- const SizedBox(height: 16),
- FormFieldRow(
- label: l10n.get('accountName'),
- value: _accountNameController.text,
- readOnly: true,
- showArrow: false,
- ),
- const SizedBox(height: 16),
- FormFieldRow(
- label: l10n.get('bankAccount'),
- value: state.expense.bankAccount.isNotEmpty
- ? state.expense.bankAccount
- : null,
- hint: l10n.get('enterBankAccount'),
- onTap: () {
- TDToast.showText(l10n.get('bankAccountInput'), context: context);
- },
- ),
- ],
- );
- }
- // ═══ 5. 报销明细区 ═══
- Widget _buildDetailSection(
- ExpenseApplyController controller,
- ExpenseApplyState state,
- ) {
- final colors = Theme.of(context).extension<AppColorsExtension>()!;
- final l10n = AppLocalizations.of(context);
- final totalApproved = state.expense.details.fold<double>(
- 0,
- (sum, d) => sum + d.approvedAmount,
- );
- return FormSection(
- title: l10n.get('expenseDetails'),
- leadingIcon: Icons.receipt_long_outlined,
- showAction: true,
- actionText: l10n.get('add'),
- onActionTap: () => _showAddDetailDialog(controller),
- children: [
- if (state.expense.details.isEmpty)
- Padding(
- padding: const EdgeInsets.symmetric(vertical: 8),
- child: Text(
- l10n.get('noDetailHint'),
- style: TextStyle(
- fontSize: AppFontSizes.subtitle,
- color: colors.textPlaceholder,
- ),
- ),
- )
- else
- ...state.expense.details.asMap().entries.map((entry) {
- final d = entry.value;
- return Container(
- margin: const EdgeInsets.symmetric(vertical: 8),
- padding: const EdgeInsets.all(12),
- decoration: BoxDecoration(
- color: colors.bgPage,
- borderRadius: BorderRadius.circular(8),
- ),
- child: Stack(
- children: [
- Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- // 第一行:费用项目 + 金额含税
- Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Expanded(
- child: Text(
- d.expenseDesc.isNotEmpty
- ? d.expenseDesc
- : l10n.get('expenseName'),
- style: TextStyle(
- fontSize: AppFontSizes.subtitle,
- color: colors.textPrimary,
- ),
- ),
- ),
- const SizedBox(width: 8),
- Text(
- '¥${d.totalAmount.toStringAsFixed(2)}',
- style: TextStyle(
- fontSize: AppFontSizes.subtitle,
- fontWeight: FontWeight.w600,
- color: colors.amountPrimary,
- ),
- ),
- ],
- ),
- const SizedBox(height: 8),
- // 第二行(小字):金额不含税 / 核准金额 / 税金 / 税率
- Text(
- '${l10n.get('amountExcludingTax')}: ¥${d.amount.toStringAsFixed(2)} '
- '${l10n.get('approvedAmount')}: ¥${d.approvedAmount.toStringAsFixed(2)} '
- '${l10n.get('taxAmount')}: ¥${d.taxAmount.toStringAsFixed(2)} '
- '${l10n.get('taxRate')}: ${(d.taxRate * 100).toStringAsFixed(0)}%',
- style: TextStyle(
- fontSize: AppFontSizes.caption,
- color: colors.textSecondary,
- ),
- ),
- const SizedBox(height: 6),
- // 第三行(小字):客户/厂商 / 已充金额
- Text(
- '${l10n.get('customerVendor')}: ${d.customerVendorName.isNotEmpty ? d.customerVendorName : '--'} '
- '${l10n.get('offsetAmount')}: ¥${d.offsetAmount.toStringAsFixed(2)}',
- style: TextStyle(
- fontSize: AppFontSizes.caption,
- color: colors.textSecondary,
- ),
- ),
- const SizedBox(height: 6),
- // 第四行(小字):费用科目 / 项目代号 / 科目代号 / 项目类别
- Text(
- '${l10n.get('expenseCategory')}: ${d.expenseType.isNotEmpty ? d.expenseType : '--'} '
- '${l10n.get('projectCode')}: ${d.projectCode.isNotEmpty ? d.projectCode : '--'} '
- '${l10n.get('subjectCode')}: ${d.subjectCode.isNotEmpty ? d.subjectCode : '--'} '
- '${l10n.get('projectCategory')}: ${d.projectCategory.isNotEmpty ? d.projectCategory : '--'}',
- style: TextStyle(
- fontSize: AppFontSizes.caption,
- color: colors.textSecondary,
- ),
- ),
- // 第五行:摘要
- if (d.remark.isNotEmpty) ...[
- const SizedBox(height: 6),
- Text(
- d.remark,
- maxLines: 2,
- overflow: TextOverflow.ellipsis,
- style: TextStyle(
- fontSize: AppFontSizes.caption,
- color: colors.textPlaceholder,
- ),
- ),
- ],
- ],
- ),
- // 右上角圆形删除按钮
- Positioned(
- right: 0,
- top: 0,
- child: GestureDetector(
- onTap: () {
- controller.removeDetail(entry.key);
- controller.recalculateAmount();
- },
- child: Container(
- width: 24,
- height: 24,
- decoration: BoxDecoration(
- color: colors.primaryLight,
- shape: BoxShape.circle,
- ),
- child: Icon(
- Icons.close,
- size: 14,
- color: colors.primary700,
- ),
- ),
- ),
- ),
- ],
- ),
- );
- }),
- // 报销金额合计 & 报销核准金额合计(始终显示)
- const SizedBox(height: 8),
- Container(
- height: 36,
- padding: const EdgeInsets.symmetric(vertical: 8),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- Text(
- l10n.get('totalExpense'),
- style: TextStyle(
- fontSize: AppFontSizes.body,
- fontWeight: FontWeight.w600,
- color: colors.textPrimary,
- ),
- ),
- Text(
- '¥${state.expense.totalAmount.toStringAsFixed(2)}',
- style: TextStyle(
- fontSize: AppFontSizes.subtitle,
- fontWeight: FontWeight.w700,
- color: colors.amountPrimary,
- ),
- ),
- ],
- ),
- ),
- Container(
- height: 36,
- padding: const EdgeInsets.symmetric(vertical: 8),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- Text(
- l10n.get('approvedTotal'),
- style: TextStyle(
- fontSize: AppFontSizes.body,
- fontWeight: FontWeight.w600,
- color: colors.textPrimary,
- ),
- ),
- Text(
- '¥${totalApproved.toStringAsFixed(2)}',
- style: TextStyle(
- fontSize: AppFontSizes.subtitle,
- fontWeight: FontWeight.w700,
- color: colors.amountPrimary,
- ),
- ),
- ],
- ),
- ),
- ],
- );
- }
- // ═══ 6. 附件上传区 ═══
- Widget _buildInvoiceSection(
- ExpenseApplyController controller,
- ExpenseApplyState state,
- ) {
- final colors = Theme.of(context).extension<AppColorsExtension>()!;
- final l10n = AppLocalizations.of(context);
- return FormSection(
- title: l10n.get('invoiceUpload'),
- leadingIcon: Icons.attach_file_outlined,
- children: [
- Text(
- l10n.get('maxInvoices'),
- style: TextStyle(
- fontSize: AppFontSizes.caption,
- color: colors.textPlaceholder,
- ),
- ),
- const SizedBox(height: 8),
- Wrap(
- spacing: 8,
- runSpacing: 8,
- children: [
- GestureDetector(
- onTap: () {
- TDToast.showText(
- l10n.get('expenseApplyImport'),
- context: context,
- );
- },
- child: Container(
- width: 80,
- height: 80,
- decoration: BoxDecoration(
- color: colors.bgPage,
- borderRadius: BorderRadius.circular(4),
- border: Border.all(color: colors.border, width: 1),
- ),
- child: Center(
- child: Icon(
- Icons.add,
- size: 24,
- color: colors.textPlaceholder,
- ),
- ),
- ),
- ),
- ],
- ),
- ],
- );
- }
- // ═══ 7. 底部操作栏 ═══
- Widget _buildBottomButtons(
- ExpenseApplyController controller,
- ExpenseApplyState state,
- ) {
- final colors = Theme.of(context).extension<AppColorsExtension>()!;
- final l10n = AppLocalizations.of(context);
- return Container(
- height: 72,
- padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
- decoration: BoxDecoration(color: colors.bgCard),
- child: Row(
- children: [
- // 存草稿
- Expanded(
- child: SizedBox(
- height: 40,
- child: Material(
- color: colors.bgCard,
- borderRadius: BorderRadius.circular(22),
- child: InkWell(
- onTap: state.isSubmitting
- ? null
- : () async {
- final ok = await controller.saveDraft();
- if (!mounted) return;
- if (ok) {
- TDToast.showText(
- l10n.get('draftSavedToast'),
- context: context,
- );
- }
- },
- borderRadius: BorderRadius.circular(22),
- child: Center(
- child: Text(
- l10n.get('saveDraftShort'),
- style: TextStyle(
- fontSize: AppFontSizes.body,
- fontWeight: FontWeight.w500,
- color: colors.textSecondary,
- ),
- ),
- ),
- ),
- ),
- ),
- ),
- const SizedBox(width: 12),
- // 重置
- Expanded(
- child: SizedBox(
- height: 40,
- child: Material(
- color: colors.bgCard,
- borderRadius: BorderRadius.circular(22),
- child: InkWell(
- onTap: () {
- setState(() {
- _purposeController.clear();
- _remarkController.clear();
- _voucherNoController.clear();
- });
- },
- borderRadius: BorderRadius.circular(22),
- child: Center(
- child: Text(
- l10n.get('reset'),
- style: TextStyle(
- fontSize: AppFontSizes.body,
- fontWeight: FontWeight.w500,
- color: colors.textSecondary,
- ),
- ),
- ),
- ),
- ),
- ),
- ),
- const SizedBox(width: 12),
- // 保存
- Expanded(
- child: SizedBox(
- height: 40,
- child: Material(
- color: colors.primary,
- borderRadius: BorderRadius.circular(22),
- child: InkWell(
- onTap: state.isSubmitting
- ? null
- : () async {
- await controller.saveDraft();
- if (!mounted) return;
- context.pop();
- },
- borderRadius: BorderRadius.circular(22),
- child: Center(
- child: Text(
- l10n.get('save'),
- style: TextStyle(
- fontSize: AppFontSizes.body,
- fontWeight: FontWeight.w500,
- color: colors.bgCard,
- ),
- ),
- ),
- ),
- ),
- ),
- ),
- ],
- ),
- );
- }
- // ═══════════════════════════════════════════
- // 弹窗 / 选择器方法
- // ═══════════════════════════════════════════
- /// 添加报销明细对话框(底部滑出,参照 ExpenseDetailDialog 样式)
- Future<void> _showAddDetailDialog(ExpenseApplyController controller) async {
- final l10n = AppLocalizations.of(context);
- final result = await ExpenseDetailEditDialog.show(
- context,
- categories: mockCostCategories,
- l10n: l10n,
- );
- if (result != null && mounted) {
- final amountExclTax = result.taxRate > 0
- ? result.amount / (1 + result.taxRate)
- : result.amount;
- final taxAmount = result.amount - amountExclTax;
- controller.addDetail(
- ExpenseDetailModel(
- id: DateTime.now().millisecondsSinceEpoch.toString(),
- expenseId: '',
- expenseDate: DateTime.now(),
- expenseType: result.category,
- expenseDesc: result.expenseDesc,
- amount: amountExclTax,
- taxRate: result.taxRate,
- taxAmount: taxAmount,
- totalAmount: result.amount,
- customerVendorName: result.customerVendorName,
- offsetAmount: result.offsetAmount,
- remark: result.remark,
- baseAmount: result.amount,
- projectCode: _selectedProjectName ?? '',
- subjectCode: _selectedSubjectName ?? '',
- projectCategory: '',
- ),
- );
- controller.recalculateAmount();
- }
- }
- /// 币别选择器
- void _showCurrencyPicker(
- ExpenseApplyController controller,
- String currentCurrency,
- ) {
- final l10n = AppLocalizations.of(context);
- final colors = Theme.of(context).extension<AppColorsExtension>()!;
- const currencies = ['CNY', 'USD', 'EUR', 'JPY', 'HKD', 'GBP'];
- TDPicker.showMultiPicker(
- context,
- title: l10n.get('selectCurrency'),
- backgroundColor: colors.bgCard,
- data: [currencies],
- onConfirm: (selected) {
- if (selected.isNotEmpty && selected[0] is int) {
- final idx = selected[0] as int;
- if (idx >= 0 && idx < currencies.length) {
- Navigator.of(context).pop();
- controller.updateCurrencyCode(currencies[idx]);
- }
- }
- },
- );
- }
- /// 支付方式选择器
- void _showPaymentMethodPicker(ExpenseApplyController controller) {
- final l10n = AppLocalizations.of(context);
- final colors = Theme.of(context).extension<AppColorsExtension>()!;
- const methods = ['bankTransfer', 'cash', 'check', 'alipay', 'wechat'];
- TDPicker.showMultiPicker(
- context,
- title: l10n.get('selectPaymentMethod'),
- backgroundColor: colors.bgCard,
- data: [methods],
- onConfirm: (selected) {
- if (selected.isNotEmpty && selected[0] is int) {
- final idx = selected[0] as int;
- if (idx >= 0 && idx < methods.length) {
- Navigator.of(context).pop();
- // The controller can be extended with updatePaymentMethod if needed
- TDToast.showText(methods[idx], context: context);
- }
- }
- },
- );
- }
- /// 通用文本输入对话框
- void _showTextInput(
- String title,
- String hint,
- void Function(String) onConfirm, {
- String initialText = '',
- }) {
- final l10n = AppLocalizations.of(context);
- final c = TextEditingController(text: initialText);
- showGeneralDialog(
- context: context,
- pageBuilder: (ctx, animation, secondaryAnimation) => TDInputDialog(
- textEditingController: c,
- title: title,
- hintText: hint,
- leftBtn: TDDialogButtonOptions(
- title: l10n.get('cancel'),
- action: () => Navigator.pop(ctx),
- ),
- rightBtn: TDDialogButtonOptions(
- title: l10n.get('confirm'),
- action: () {
- onConfirm(c.text);
- Navigator.pop(ctx);
- },
- ),
- ),
- );
- }
- /// 获取今天的日期字符串
- String _today() {
- final n = DateTime.now();
- return '${n.year}-${n.month.toString().padLeft(2, '0')}-${n.day.toString().padLeft(2, '0')}';
- }
- }
|