| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533 |
- import 'package:flutter/material.dart';
- import 'package:flutter_riverpod/flutter_riverpod.dart';
- import 'package:go_router/go_router.dart';
- import 'package:tdesign_flutter/tdesign_flutter.dart';
- import '../../core/theme/app_colors.dart';
- import '../../core/i18n/app_localizations.dart';
- import '../../shared/widgets/action_bar.dart';
- import '../../shared/widgets/form_section.dart';
- import '../../shared/widgets/form_field_row.dart';
- import '../shell/nav_bar_config.dart';
- class ExpenseApplicationApplyPage extends ConsumerStatefulWidget {
- final String? id;
- const ExpenseApplicationApplyPage({super.key, this.id});
- @override
- ConsumerState<ExpenseApplicationApplyPage> createState() =>
- _ExpenseApplicationApplyPageState();
- }
- class _ExpenseApplicationApplyPageState
- extends ConsumerState<ExpenseApplicationApplyPage> {
- // ── 基本信息 ──
- int _urgency = 0; // 0=普通, 1=紧急, 2=特急
- static const _urgencyLabels = ['普通', '紧急', '特急'];
- final Set<String> _expenseTypes = {};
- static const _expenseTypeOptions = [
- ('travel', '差旅费'),
- ('entertainment', '业务招待费'),
- ('procurement', '日常采购'),
- ('activity', '活动经费'),
- ('office', '办公费'),
- ('meeting', '会议费'),
- ('training', '培训费'),
- ];
- bool _isTaxIncluded = false;
- final _purposeController = TextEditingController();
- // ── 关联管控 ──
- String? _selectedProjectName;
- int? _selectedProjectId;
- String? _selectedSubjectName;
- int? _selectedSubjectId;
- final _availableBudget = 50000.00;
- final _referenceNoController = TextEditingController();
- // ── 费用明细 ──
- final List<_DetailItem> _details = [];
- int _detailIdCounter = 1;
- // ── 附件 ──
- final List<String> _attachments = []; // mock file names
- // ── 专用字段 ──
- String _estimatedStartDate = '';
- String _estimatedEndDate = '';
- String _entertainmentTarget = '';
- String _venue = '';
- @override
- void dispose() {
- _purposeController.dispose();
- _referenceNoController.dispose();
- super.dispose();
- }
- @override
- Widget build(BuildContext context) {
- final l10n = AppLocalizations.of(context);
- ref.read(navBarConfigProvider.notifier).update(
- NavBarConfig(
- title: l10n.get('expenseApplyRequest'),
- showBack: true,
- onBack: () {
- if (_hasUnsaved()) {
- _showConfirmDialog('确认退出', '当前内容尚未保存,是否退出?', '继续编辑', '放弃并退出', () => context.pop());
- } else {
- context.pop();
- }
- },
- ),
- );
- return PopScope(
- canPop: false,
- onPopInvokedWithResult: (didPop, _) {
- if (!didPop) {
- if (_hasUnsaved()) {
- _showConfirmDialog('确认退出', '当前内容尚未保存,是否退出?', '继续编辑', '放弃并退出', () => context.pop());
- } else {
- context.pop();
- }
- }
- },
- child: Column(
- children: [
- Expanded(
- child: SingleChildScrollView(
- padding: const EdgeInsets.all(16),
- child: Column(
- children: [
- _buildBasicInfo(l10n),
- const SizedBox(height: 16),
- _buildTypeSpecificFields(l10n),
- const SizedBox(height: 16),
- _buildControlSection(l10n),
- const SizedBox(height: 16),
- _buildDetailsSection(l10n),
- const SizedBox(height: 16),
- _buildAttachmentSection(l10n),
- const SizedBox(height: 80),
- ],
- ),
- ),
- ),
- _buildBottomBar(l10n),
- ],
- ),
- );
- }
- // ═══ 1. 基本信息 ═══
- Widget _buildBasicInfo(AppLocalizations l10n) {
- return FormSection(
- title: l10n.get('basicInfo'),
- children: [
- FormFieldRow(label: l10n.get('applicant'), value: '张三', readOnly: true, showArrow: false),
- FormFieldRow(label: l10n.get('department'), value: '技术部', readOnly: true, showArrow: false),
- FormFieldRow(label: l10n.get('date'), value: _today(), readOnly: true, showArrow: false),
- const SizedBox(height: 12),
- _label(l10n.get('emergencyLevel')),
- const SizedBox(height: 6),
- _buildUrgencyRadio(),
- const SizedBox(height: 12),
- _label(l10n.get('expenseType')),
- const SizedBox(height: 6),
- Wrap(
- spacing: 8, runSpacing: 8,
- children: _expenseTypeOptions.map((opt) {
- final sel = _expenseTypes.contains(opt.$1);
- return GestureDetector(
- onTap: () => setState(() => sel ? _expenseTypes.remove(opt.$1) : _expenseTypes.add(opt.$1)),
- child: TDTag(opt.$2, size: TDTagSize.medium, theme: sel ? TDTagTheme.primary : TDTagTheme.defaultTheme, isOutline: !sel),
- );
- }).toList(),
- ),
- const SizedBox(height: 12),
- Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
- _label(l10n.get('isTaxIncluded')),
- TDSwitch(isOn: _isTaxIncluded, onChanged: (v) { setState(() => _isTaxIncluded = v); return true; }),
- ]),
- const SizedBox(height: 12),
- _label(l10n.get('feeReason')),
- const SizedBox(height: 4),
- TDTextarea(
- controller: _purposeController,
- hintText: l10n.get('enterFeeReason'),
- maxLength: 200,
- backgroundColor: AppColors.bgPage,
- ),
- const SizedBox(height: 12),
- FormFieldRow(label: l10n.get('validUntil'), hint: l10n.get('pleaseSelect'), onTap: () => _pickDate((d) {})),
- ],
- );
- }
- Widget _buildUrgencyRadio() {
- return Row(
- children: List.generate(3, (i) {
- final sel = _urgency == i;
- return Padding(
- padding: EdgeInsets.only(right: i < 2 ? 24 : 0),
- child: GestureDetector(
- onTap: () => setState(() => _urgency = i),
- child: Row(mainAxisSize: MainAxisSize.min, children: [
- Container(
- width: 18, height: 18,
- decoration: BoxDecoration(shape: BoxShape.circle, border: Border.all(color: sel ? AppColors.primary : AppColors.textPlaceholder, width: 2)),
- child: sel ? Center(child: Container(width: 8, height: 8, decoration: const BoxDecoration(shape: BoxShape.circle, color: AppColors.primary))) : null,
- ),
- const SizedBox(width: 6),
- Text(_urgencyLabels[i], style: TextStyle(fontSize: AppFontSizes.body, color: sel ? AppColors.primary : AppColors.textSecondary)),
- ]),
- ),
- );
- }),
- );
- }
- // ═══ 2. 类型专用字段 ═══
- Widget _buildTypeSpecificFields(AppLocalizations l10n) {
- final ws = <Widget>[];
- if (_expenseTypes.contains('travel')) ws.add(_buildTravelFields(l10n));
- if (_expenseTypes.contains('entertainment')) { ws.add(const SizedBox(height: 16)); ws.add(_buildEntertainmentFields(l10n)); }
- if (_expenseTypes.contains('meeting')) { ws.add(const SizedBox(height: 16)); ws.add(_buildMeetingFields(l10n)); }
- return ws.isEmpty ? const SizedBox.shrink() : Column(children: ws);
- }
- Widget _buildTravelFields(AppLocalizations l10n) {
- return FormSection(title: '差旅费专用', children: [
- FormFieldRow(label: l10n.get('estimatedStartDate'), value: _estimatedStartDate, hint: l10n.get('pleaseSelect'), onTap: () => _pickDate((d) => setState(() => _estimatedStartDate = d))),
- FormFieldRow(label: l10n.get('estimatedEndDate'), value: _estimatedEndDate, hint: l10n.get('pleaseSelect'), onTap: () => _pickDate((d) => setState(() => _estimatedEndDate = d))),
- const SizedBox(height: 8),
- Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [_label(l10n.get('isOvernight')), TDSwitch(onChanged: (_) => true)]),
- const SizedBox(height: 8),
- FormFieldRow(label: l10n.get('transportType'), value: '高铁/动车', onTap: () => _showListPicker('选择交通工具', ['飞机', '高铁/动车', '火车(普速)', '自驾'], (_) {})),
- ]);
- }
- Widget _buildEntertainmentFields(AppLocalizations l10n) {
- return FormSection(title: '招待费专用', children: [
- FormFieldRow(label: '招待对象单位', value: _entertainmentTarget, hint: '请输入', onTap: () => _showTextInput('招待对象单位', (v) => setState(() => _entertainmentTarget = v))),
- FormFieldRow(label: '招待层级', value: '普通', onTap: () => _showListPicker('选择招待层级', ['普通', '重要', 'VIP'], (_) {})),
- FormFieldRow(label: '外部人数', value: '3', onTap: () => _showNumberInput('外部人数', (_) {})),
- FormFieldRow(label: '内部陪同人数', value: '2', onTap: () => _showNumberInput('内部陪同人数', (_) {})),
- FormFieldRow(label: l10n.get('venue'), value: _venue, hint: '请输入地点', onTap: () => _showTextInput(l10n.get('venue'), (v) => setState(() => _venue = v))),
- ]);
- }
- Widget _buildMeetingFields(AppLocalizations l10n) {
- return FormSection(title: '会议费专用', children: [
- FormFieldRow(label: l10n.get('estimatedStartDate'), hint: l10n.get('pleaseSelect'), onTap: () => _pickDate((_) {})),
- FormFieldRow(label: l10n.get('estimatedEndDate'), hint: l10n.get('pleaseSelect'), onTap: () => _pickDate((_) {})),
- FormFieldRow(label: l10n.get('venue'), value: _venue, hint: '请输入会议地点', onTap: () => _showTextInput('会议地点', (_) {})),
- ]);
- }
- // ═══ 3. 关联管控 ═══
- static const _mockProjects = [('华东市场拓展', 100), ('ERP系统升级', 101), ('新产品研发', 102), ('华南渠道建设', 103)];
- static const _mockSubjects = [('差旅费', 5), ('招待费', 6), ('办公费', 7), ('培训费', 8)];
- Widget _buildControlSection(AppLocalizations l10n) {
- return FormSection(title: l10n.get('relatedControl'), children: [
- FormFieldRow(label: l10n.get('relatedProject'), value: _selectedProjectName, hint: l10n.get('selectProject'), onTap: () {
- _showListPicker('选择关联项目', _mockProjects.map((p) => p.$1).toList(), (v) {
- final p = _mockProjects.firstWhere((x) => x.$1 == v);
- setState(() { _selectedProjectId = p.$2; _selectedProjectName = p.$1; _selectedSubjectName = null; _selectedSubjectId = null; });
- });
- }),
- FormFieldRow(
- label: l10n.get('budgetSubject'),
- value: _selectedSubjectName,
- hint: l10n.get('selectSubject'),
- onTap: _selectedProjectId != null
- ? () {
- _showListPicker('选择预算科目', _mockSubjects.map((s) => s.$1).toList(), (v) {
- final s = _mockSubjects.firstWhere((x) => x.$1 == v);
- setState(() { _selectedSubjectId = s.$2; _selectedSubjectName = s.$1; });
- });
- }
- : null,
- ),
- _buildBudgetRow(l10n),
- const SizedBox(height: 8),
- FormFieldRow(label: '关联合同号', value: _referenceNoController.text, hint: '选填', onTap: () => _showTextInput('关联合同号', (v) => setState(() { _referenceNoController.text = v; _referenceNoController.selection = TextSelection.fromPosition(TextPosition(offset: v.length)); }))),
- ]);
- }
- Widget _buildBudgetRow(AppLocalizations l10n) {
- final over = _totalAmount() > _availableBudget;
- return SizedBox(
- height: 44,
- child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
- Text(l10n.get('availableBudget'), style: const TextStyle(fontSize: AppFontSizes.body, color: AppColors.textSecondary)),
- Text('¥${_availableBudget.toStringAsFixed(2)}', style: TextStyle(fontSize: AppFontSizes.subtitle, fontWeight: FontWeight.w700, color: over ? AppColors.danger : AppColors.amountPrimary)),
- ]),
- );
- }
- // ═══ 4. 费用明细 ═══
- Widget _buildDetailsSection(AppLocalizations l10n) {
- return FormSection(title: l10n.get('expenseDetails'), showAction: true, actionText: l10n.get('add'), onActionTap: _showDetailDialog, children: [
- if (_details.isEmpty)
- Padding(padding: const EdgeInsets.symmetric(vertical: 8), child: Text(l10n.get('noDetailHint'), style: const TextStyle(fontSize: AppFontSizes.body, color: AppColors.textPlaceholder)))
- else
- ..._details.asMap().entries.map((e) {
- final d = e.value;
- return Container(
- padding: const EdgeInsets.symmetric(vertical: 6),
- decoration: BoxDecoration(border: e.key < _details.length - 1 ? const Border(bottom: BorderSide(color: AppColors.border)) : null),
- child: Row(children: [
- Expanded(flex: 3, child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
- Text(d.categoryName, style: const TextStyle(fontSize: AppFontSizes.body, color: AppColors.textPrimary)),
- if (d.remark.isNotEmpty) Text(d.remark, style: const TextStyle(fontSize: AppFontSizes.caption, color: AppColors.textPlaceholder)),
- ])),
- Text('${d.quantity}×¥${d.unitPrice.toStringAsFixed(2)}', style: const TextStyle(fontSize: AppFontSizes.caption, color: AppColors.textSecondary)),
- const SizedBox(width: 8),
- Text('¥${d.amount.toStringAsFixed(2)}', style: const TextStyle(fontSize: AppFontSizes.body, fontWeight: FontWeight.w600, color: AppColors.amountPrimary)),
- GestureDetector(onTap: () => setState(() => _details.removeAt(e.key)), child: const Icon(Icons.close, size: 16, color: AppColors.textPlaceholder)),
- ]),
- );
- }),
- Container(height: 1, color: AppColors.border),
- Container(height: 36, padding: const EdgeInsets.symmetric(vertical: 8), child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
- Text(l10n.get('total'), style: const TextStyle(fontSize: AppFontSizes.body, fontWeight: FontWeight.w600, color: AppColors.textPrimary)),
- Text('¥${_totalAmount().toStringAsFixed(2)}', style: const TextStyle(fontSize: AppFontSizes.subtitle, fontWeight: FontWeight.w700, color: AppColors.amountPrimary)),
- ])),
- if (_totalAmount() > _availableBudget)
- Padding(padding: const EdgeInsets.only(top: 8), child: Row(children: [
- const Icon(Icons.warning_amber, size: 14, color: AppColors.danger), const SizedBox(width: 6),
- Expanded(child: Text('您的申请金额已超支,提交后将自动触发高管特批流程', style: const TextStyle(fontSize: AppFontSizes.caption, color: AppColors.danger))),
- ])),
- ]);
- }
- double _totalAmount() => _details.fold(0, (s, d) => s + d.amount);
- static const _detailCategories = [('transport', '交通费'), ('hotel', '住宿费'), ('office_supplies', '办公用品'), ('meals', '餐饮费'), ('materials', '材料费'), ('service', '服务费'), ('other', '其他')];
- static const _units = ['张', '间', '人', '天', '套', '个'];
- void _showDetailDialog() {
- String cat = 'transport';
- String unit = '张';
- final qtyCtrl = TextEditingController(text: '1');
- final priceCtrl = TextEditingController();
- final remarkCtrl = TextEditingController();
- showDialog(
- context: context,
- builder: (ctx) => StatefulBuilder(
- builder: (ctx, setDlg) => TDAlertDialog(
- title: '添加费用明细',
- contentWidget: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [
- _label('费用类别'),
- const SizedBox(height: 4),
- GestureDetector(
- onTap: () {
- Navigator.pop(ctx);
- _showListPicker('选择费用类别', _detailCategories.map((c) => c.$2).toList(), (v) {
- cat = _detailCategories.firstWhere((c) => c.$2 == v).$1;
- _showDetailDialog();
- });
- },
- child: Container(
- height: 44, padding: const EdgeInsets.symmetric(horizontal: 12),
- decoration: BoxDecoration(color: AppColors.bgPage, borderRadius: BorderRadius.circular(4)),
- child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
- Text(_detailCategories.firstWhere((c) => c.$1 == cat).$2, style: const TextStyle(fontSize: AppFontSizes.body)),
- const Icon(Icons.arrow_drop_down, color: AppColors.textPlaceholder),
- ]),
- ),
- ),
- const SizedBox(height: 12),
- Row(children: [
- Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
- _label('数量'), const SizedBox(height: 4),
- TDInput(controller: qtyCtrl, hintText: '>0'),
- ])),
- const SizedBox(width: 12),
- Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
- _label('单位'), const SizedBox(height: 4),
- GestureDetector(
- onTap: () {
- Navigator.pop(ctx);
- _showListPicker('选择单位', _units, (v) { unit = v; _showDetailDialog(); });
- },
- child: Container(
- height: 44, padding: const EdgeInsets.symmetric(horizontal: 12),
- decoration: BoxDecoration(color: AppColors.bgPage, borderRadius: BorderRadius.circular(4)),
- child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
- Text(unit, style: const TextStyle(fontSize: AppFontSizes.body)),
- const Icon(Icons.arrow_drop_down, color: AppColors.textPlaceholder),
- ]),
- ),
- ),
- ])),
- ]),
- const SizedBox(height: 12),
- _label('单价'), const SizedBox(height: 4),
- TDInput(controller: priceCtrl, hintText: '>0'),
- const SizedBox(height: 12),
- _label('明细说明'), const SizedBox(height: 4),
- TDInput(controller: remarkCtrl, hintText: '选填'),
- ]),
- leftBtn: TDDialogButtonOptions(title: '取消', action: () => Navigator.pop(ctx)),
- rightBtn: TDDialogButtonOptions(title: '确定', titleColor: AppColors.primary, action: () {
- final q = int.tryParse(qtyCtrl.text) ?? 0;
- final p = double.tryParse(priceCtrl.text) ?? 0;
- if (q <= 0 || p <= 0) { TDToast.showText('数量和单价必须大于0', context: context); return; }
- setState(() => _details.add(_DetailItem(id: _detailIdCounter++, category: cat, categoryName: _detailCategories.firstWhere((c) => c.$1 == cat).$2, quantity: q, unit: unit, unitPrice: p, amount: q * p, remark: remarkCtrl.text)));
- Navigator.pop(ctx);
- }),
- ),
- ),
- );
- }
- // ═══ 5. 附件上传 ═══
- Widget _buildAttachmentSection(AppLocalizations l10n) {
- return FormSection(title: l10n.get('attachmentUpload'), children: [
- Text(l10n.get('maxAttachment'), style: const TextStyle(fontSize: AppFontSizes.caption, color: AppColors.textPlaceholder)),
- const SizedBox(height: 12),
- Wrap(spacing: 8, runSpacing: 8, children: [
- ..._attachments.asMap().entries.map((e) => Stack(
- clipBehavior: Clip.none,
- children: [
- Container(width: 80, height: 80, decoration: BoxDecoration(color: AppColors.primaryLight, borderRadius: BorderRadius.circular(4)),
- child: const Center(child: Icon(Icons.image, color: AppColors.primary, size: 32)),
- ),
- Positioned(right: -4, top: -4, child: GestureDetector(
- onTap: () => setState(() => _attachments.removeAt(e.key)),
- child: Container(width: 20, height: 20, decoration: const BoxDecoration(color: AppColors.danger, shape: BoxShape.circle),
- child: const Icon(Icons.close, size: 12, color: Colors.white),
- ),
- )),
- ],
- )),
- if (_attachments.length < 9)
- GestureDetector(
- onTap: () {
- // Mock: add attachment
- setState(() => _attachments.add('附件_${DateTime.now().millisecondsSinceEpoch}.jpg'));
- TDToast.showText('已添加附件(mock)', context: context);
- },
- child: Container(width: 80, height: 80, decoration: BoxDecoration(color: AppColors.bgPage, borderRadius: BorderRadius.circular(4), border: Border.all(color: AppColors.border)),
- child: const Center(child: Icon(Icons.add, size: 24, color: AppColors.textPlaceholder)),
- ),
- ),
- ]),
- ]);
- }
- // ═══ 6. 底部操作栏 ═══
- Widget _buildBottomBar(AppLocalizations l10n) {
- final isDraft = widget.id != null;
- return ActionBar(
- leftLabel: isDraft ? l10n.get('reset') : null,
- centerLabel: l10n.get('saveDraft'),
- rightLabel: l10n.get('submitApproval'),
- showLeft: isDraft,
- onLeftTap: isDraft ? () => _showConfirmDialog('确认重置', '将清空所有已填内容,此操作不可撤销', '取消', '确认重置', _resetAll) : null,
- onCenterTap: () {
- TDToast.showSuccess('已保存为草稿', context: context);
- context.pop();
- },
- onRightTap: () {
- final err = _validate();
- if (err.isNotEmpty) { TDToast.showText(err.first, context: context); return; }
- TDToast.showSuccess('已提交,等待审批', context: context);
- context.pop();
- },
- );
- }
- List<String> _validate() {
- final e = <String>[];
- if (_expenseTypes.isEmpty) e.add('请至少选择一项费用类型');
- if (_purposeController.text.trim().isEmpty) e.add('请填写费用事由');
- if (_selectedProjectId == null) e.add('请选择关联项目');
- if (_selectedSubjectId == null) e.add('请选择预算科目');
- if (_details.isEmpty) e.add('请至少添加一行费用明细');
- if (_expenseTypes.contains('travel')) {
- if (_estimatedStartDate.isEmpty) e.add('请选择预计开始日期');
- if (_estimatedEndDate.isEmpty) e.add('请选择预计结束日期');
- }
- return e;
- }
- void _resetAll() => setState(() {
- _purposeController.clear(); _expenseTypes.clear(); _urgency = 0; _isTaxIncluded = false;
- _selectedProjectId = null; _selectedProjectName = null; _selectedSubjectId = null; _selectedSubjectName = null;
- _referenceNoController.clear(); _details.clear(); _attachments.clear();
- _estimatedStartDate = ''; _estimatedEndDate = ''; _entertainmentTarget = ''; _venue = '';
- });
- bool _hasUnsaved() => _purposeController.text.isNotEmpty || _expenseTypes.isNotEmpty || _details.isNotEmpty || _attachments.isNotEmpty || _selectedProjectId != null;
- // ═══ 通用弹窗方法 ═══
- void _showConfirmDialog(String title, String content, String leftText, String rightText, VoidCallback onConfirm) {
- showDialog(
- context: context,
- builder: (ctx) => TDAlertDialog(
- title: title,
- content: content,
- leftBtn: TDDialogButtonOptions(title: leftText, titleColor: AppColors.primary, action: () => Navigator.pop(ctx)),
- rightBtn: TDDialogButtonOptions(title: rightText, titleColor: AppColors.danger, action: () { Navigator.pop(ctx); onConfirm(); }),
- ),
- );
- }
- void _showListPicker(String title, List<String> items, Function(String) onPick) {
- showDialog(
- context: context,
- builder: (ctx) => TDAlertDialog(
- title: title,
- contentWidget: SizedBox(width: double.maxFinite, child: ListView(shrinkWrap: true, children: items.map((item) => ListTile(title: Text(item), onTap: () { onPick(item); Navigator.pop(ctx); })).toList())),
- leftBtn: TDDialogButtonOptions(title: '取消', action: () => Navigator.pop(ctx)),
- ),
- );
- }
- void _showTextInput(String title, Function(String) onConfirm) {
- final c = TextEditingController();
- showDialog(
- context: context,
- builder: (ctx) => TDAlertDialog(
- title: title,
- contentWidget: TDInput(controller: c, hintText: '请输入'),
- leftBtn: TDDialogButtonOptions(title: '取消', action: () => Navigator.pop(ctx)),
- rightBtn: TDDialogButtonOptions(title: '确定', titleColor: AppColors.primary, action: () { onConfirm(c.text); Navigator.pop(ctx); }),
- ),
- );
- }
- void _showNumberInput(String title, Function(int) onConfirm) {
- final c = TextEditingController();
- showDialog(
- context: context,
- builder: (ctx) => TDAlertDialog(
- title: title,
- contentWidget: TextField(controller: c, keyboardType: TextInputType.number, decoration: const InputDecoration(hintText: '请输入数字')),
- leftBtn: TDDialogButtonOptions(title: '取消', action: () => Navigator.pop(ctx)),
- rightBtn: TDDialogButtonOptions(title: '确定', titleColor: AppColors.primary, action: () { onConfirm(int.tryParse(c.text) ?? 0); Navigator.pop(ctx); }),
- ),
- );
- }
- void _pickDate(Function(String) onPick) {
- showDatePicker(context: context, initialDate: DateTime.now(), firstDate: DateTime(2024), lastDate: DateTime(2028), locale: const Locale('zh'))
- .then((d) { if (d != null) onPick('${d.year}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')}'); });
- }
- Widget _label(String t) => Text(t, style: const TextStyle(fontSize: AppFontSizes.body, color: AppColors.textSecondary));
- String _today() { final n = DateTime.now(); return '${n.year}-${n.month.toString().padLeft(2, '0')}-${n.day.toString().padLeft(2, '0')}'; }
- }
- class _DetailItem {
- final int id; final String category; final String categoryName; final int quantity;
- final String unit; final double unitPrice; final double amount; final String remark;
- const _DetailItem({required this.id, required this.category, required this.categoryName, required this.quantity, required this.unit, required this.unitPrice, required this.amount, required this.remark});
- }
|