Преглед изворни кода

fix #19407 金额相关优化、状态表达优化

chengc пре 1 недеља
родитељ
комит
0c6503438d

+ 2 - 2
assets/i18n/zh_CN.json

@@ -62,7 +62,7 @@
     "preview": "预览",
     "preview": "预览",
     "previewTitle": "预览",
     "previewTitle": "预览",
     "draft": "草稿",
     "draft": "草稿",
-    "pending": "审中",
+    "pending": "审中",
     "approved": "已通过",
     "approved": "已通过",
     "rejected": "已拒绝",
     "rejected": "已拒绝",
     "expired": "已过期",
     "expired": "已过期",
@@ -70,7 +70,7 @@
     "unpaid": "待付款",
     "unpaid": "待付款",
     "returned": "已还车",
     "returned": "已还车",
     "completed": "已完成",
     "completed": "已完成",
-    "statusPending": "审中",
+    "statusPending": "审中",
     "statusApproved": "审核通过",
     "statusApproved": "审核通过",
     "statusRejected": "已拒绝",
     "statusRejected": "已拒绝",
     "statusDraft": "草稿",
     "statusDraft": "草稿",

+ 2 - 2
assets/i18n/zh_TW.json

@@ -65,7 +65,7 @@
     "preview": "預覽",
     "preview": "預覽",
     "previewTitle": "預覽",
     "previewTitle": "預覽",
     "draft": "草稿",
     "draft": "草稿",
-    "pending": "審中",
+    "pending": "審中",
     "approved": "已通過",
     "approved": "已通過",
     "rejected": "已拒絕",
     "rejected": "已拒絕",
     "expired": "已過期",
     "expired": "已過期",
@@ -73,7 +73,7 @@
     "unpaid": "待付款",
     "unpaid": "待付款",
     "returned": "已還車",
     "returned": "已還車",
     "completed": "已完成",
     "completed": "已完成",
-    "statusPending": "審中",
+    "statusPending": "審中",
     "statusApproved": "審核通過",
     "statusApproved": "審核通過",
     "statusRejected": "已拒絕",
     "statusRejected": "已拒絕",
     "statusDraft": "草稿",
     "statusDraft": "草稿",

+ 4 - 0
lib/core/navigation/host_app_channel.dart

@@ -23,6 +23,7 @@ class HostAppChannel {
   static String? _depName;
   static String? _depName;
   static String? _usr;
   static String? _usr;
   static String? _usrName;
   static String? _usrName;
+  static int? _poiAmt;
 
 
   static String get baseUrl => _baseUrl ?? '';
   static String get baseUrl => _baseUrl ?? '';
   static String get sn => _sn ?? '';
   static String get sn => _sn ?? '';
@@ -31,6 +32,7 @@ class HostAppChannel {
   static String get depName => _depName ?? '';
   static String get depName => _depName ?? '';
   static String get usr => _usr ?? '';
   static String get usr => _usr ?? '';
   static String get usrName => _usrName ?? '';
   static String get usrName => _usrName ?? '';
+  static int get poiAmt => _poiAmt ?? 2;
 
 
   static bool _initialized = false;
   static bool _initialized = false;
   static bool get isInitialized => _initialized;
   static bool get isInitialized => _initialized;
@@ -59,6 +61,7 @@ class HostAppChannel {
         _depName = result['depName'] as String?;
         _depName = result['depName'] as String?;
         _usr = result['usr'] as String?;
         _usr = result['usr'] as String?;
         _usrName = result['usrName'] as String?;
         _usrName = result['usrName'] as String?;
+        _poiAmt = result['poiAmt'] as int?;
       }
       }
     } catch (_) {
     } catch (_) {
       // 原生端 MethodChannel handler 尚未注册,稍后重试
       // 原生端 MethodChannel handler 尚未注册,稍后重试
@@ -119,6 +122,7 @@ class HostAppChannel {
         _depName = result['depName'] as String? ?? _depName;
         _depName = result['depName'] as String? ?? _depName;
         _usr = result['usr'] as String? ?? _usr;
         _usr = result['usr'] as String? ?? _usr;
         _usrName = result['usrName'] as String? ?? _usrName;
         _usrName = result['usrName'] as String? ?? _usrName;
+        _poiAmt = result['poiAmt'] as int? ?? _poiAmt;
         _initialized = (_baseUrl != null && _baseUrl!.isNotEmpty);
         _initialized = (_baseUrl != null && _baseUrl!.isNotEmpty);
       }
       }
     } catch (_) {
     } catch (_) {

+ 18 - 0
lib/core/utils/amount_utils.dart

@@ -0,0 +1,18 @@
+import 'package:intl/intl.dart';
+import '../navigation/host_app_channel.dart';
+
+/// 显示用金额格式化:千分位分隔 + ERP 动态小数位(无 ¥ 前缀)。
+/// 示例: 1234567.89 → "1,234,567.89"(poiAmt=2)
+String formatAmount(num amount) {
+  if (amount.isNaN || amount.isInfinite) return '0';
+  final digits = HostAppChannel.poiAmt;
+  final formatter = NumberFormat('#,##0.${'0' * digits}', 'en_US');
+  return formatter.format(amount);
+}
+
+/// 输入框用金额格式化:仅保留 ERP 动态小数位,无千分位分隔。
+/// 示例: 1234567.89 → "1234567.89"(poiAmt=2)
+String formatAmountNumeric(num amount) {
+  if (amount.isNaN || amount.isInfinite) return '0';
+  return amount.toStringAsFixed(HostAppChannel.poiAmt);
+}

+ 18 - 69
lib/features/expense/expense_apply_import_page.dart

@@ -1,8 +1,9 @@
-import 'package:flutter/material.dart';
+import 'package:flutter/material.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
 import 'package:tdesign_flutter/tdesign_flutter.dart';
 import 'package:tdesign_flutter/tdesign_flutter.dart';
 import 'package:easy_refresh/easy_refresh.dart';
 import 'package:easy_refresh/easy_refresh.dart';
 import '../../core/i18n/app_localizations.dart';
 import '../../core/i18n/app_localizations.dart';
+import '../../core/utils/amount_utils.dart';
 import '../../core/theme/app_colors_extension.dart';
 import '../../core/theme/app_colors_extension.dart';
 import '../../shared/widgets/empty_state.dart';
 import '../../shared/widgets/empty_state.dart';
 import '../../shared/widgets/app_skeletons.dart';
 import '../../shared/widgets/app_skeletons.dart';
@@ -103,7 +104,7 @@ class ExpenseApplyImportPage extends ConsumerStatefulWidget {
 
 
 class _ExpenseApplyImportPageState extends ConsumerState<ExpenseApplyImportPage>
 class _ExpenseApplyImportPageState extends ConsumerState<ExpenseApplyImportPage>
     with WidgetsBindingObserver {
     with WidgetsBindingObserver {
-  final _aeNoCtrl = TextEditingController();
+  String _aeNoKeyword = '';
   final _startDateCtrl = TextEditingController();
   final _startDateCtrl = TextEditingController();
   final _endDateCtrl = TextEditingController();
   final _endDateCtrl = TextEditingController();
   List<ImportableItem> _items = [];
   List<ImportableItem> _items = [];
@@ -133,7 +134,6 @@ class _ExpenseApplyImportPageState extends ConsumerState<ExpenseApplyImportPage>
   @override
   @override
   void dispose() {
   void dispose() {
     WidgetsBinding.instance.removeObserver(this);
     WidgetsBinding.instance.removeObserver(this);
-    _aeNoCtrl.dispose();
     _startDateCtrl.dispose();
     _startDateCtrl.dispose();
     _endDateCtrl.dispose();
     _endDateCtrl.dispose();
     _scrollCtrl.dispose();
     _scrollCtrl.dispose();
@@ -145,7 +145,7 @@ class _ExpenseApplyImportPageState extends ConsumerState<ExpenseApplyImportPage>
   void didChangeAppLifecycleState(AppLifecycleState state) {
   void didChangeAppLifecycleState(AppLifecycleState state) {
     if (state == AppLifecycleState.resumed) {
     if (state == AppLifecycleState.resumed) {
       FocusScope.of(context).unfocus();
       FocusScope.of(context).unfocus();
-      _aeNoCtrl.clear();
+      _aeNoKeyword = '';
       final now = DateTime.now();
       final now = DateTime.now();
       _startDateCtrl.text =
       _startDateCtrl.text =
           '${now.year}-${now.month.toString().padLeft(2, '0')}-01';
           '${now.year}-${now.month.toString().padLeft(2, '0')}-01';
@@ -178,7 +178,7 @@ class _ExpenseApplyImportPageState extends ConsumerState<ExpenseApplyImportPage>
     try {
     try {
       final api = ref.read(expenseApiProvider);
       final api = ref.read(expenseApiProvider);
       final result = await api.getImportableExpenseApplyList(
       final result = await api.getImportableExpenseApplyList(
-        keyword: _aeNoCtrl.text.trim(),
+        keyword: _aeNoKeyword,
         startDate: _startDateCtrl.text,
         startDate: _startDateCtrl.text,
         endDate: _endDateCtrl.text,
         endDate: _endDateCtrl.text,
         page: append ? _page : 1,
         page: append ? _page : 1,
@@ -319,7 +319,7 @@ class _ExpenseApplyImportPageState extends ConsumerState<ExpenseApplyImportPage>
         mainAxisSize: MainAxisSize.min,
         mainAxisSize: MainAxisSize.min,
         children: [
         children: [
           Padding(
           Padding(
-            padding: const EdgeInsets.fromLTRB(12, 8, 12, 0),
+            padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
             child: Row(
             child: Row(
               children: [
               children: [
                 Expanded(
                 Expanded(
@@ -355,56 +355,22 @@ class _ExpenseApplyImportPageState extends ConsumerState<ExpenseApplyImportPage>
           ),
           ),
           const SizedBox(height: 8),
           const SizedBox(height: 8),
           Padding(
           Padding(
-            padding: const EdgeInsets.fromLTRB(12, 0, 12, 8),
+            padding: const EdgeInsets.fromLTRB(0, 0, 12, 8),
             child: Row(
             child: Row(
               children: [
               children: [
                 Expanded(
                 Expanded(
-                  child: Container(
-                    height: 40,
-                    padding: const EdgeInsets.symmetric(horizontal: 16),
-                    decoration: BoxDecoration(
-                      color: colors.bgSecondaryContainer,
-                      borderRadius: BorderRadius.circular(20),
-                      border: Border.all(color: tdTheme.componentStrokeColor),
-                    ),
-                    child: Row(
-                      children: [
-                        Expanded(
-                          child: TextField(
-                            controller: _aeNoCtrl,
-                            style: TextStyle(
-                              fontSize: 14,
-                              color: colors.textPrimary,
-                            ),
-                            decoration: InputDecoration(
-                              hintText: l10n.get('searchImportHint'),
-                              hintStyle: TextStyle(
-                                fontSize: 14,
-                                color: colors.textSecondary,
-                              ),
-                              border: InputBorder.none,
-                              isCollapsed: true,
-                            ),
-                            onChanged: (_) => setState(() {}),
-                          ),
-                        ),
-                        if (_aeNoCtrl.text.isNotEmpty)
-                          GestureDetector(
-                            onTap: () {
-                              _aeNoCtrl.clear();
-                              setState(() {});
-                            },
-                            child: Icon(
-                              Icons.close,
-                              size: 18,
-                              color: colors.textSecondary,
-                            ),
-                          ),
-                      ],
-                    ),
+                  child: TDSearchBar(
+                    placeHolder: l10n.get('searchImportHint'),
+                    style: TDSearchStyle.round,
+                    onTextChanged: (String text) {
+                      setState(() {
+                        _aeNoKeyword = text;
+                      });
+                      if (text.isEmpty) _search();
+                    },
+                    onSubmitted: (_) => _search(),
                   ),
                   ),
                 ),
                 ),
-                const SizedBox(width: 8),
                 GestureDetector(
                 GestureDetector(
                   onTap: () {
                   onTap: () {
                     setState(() => _sortDesc = !_sortDesc);
                     setState(() => _sortDesc = !_sortDesc);
@@ -426,23 +392,6 @@ class _ExpenseApplyImportPageState extends ConsumerState<ExpenseApplyImportPage>
                     ),
                     ),
                   ),
                   ),
                 ),
                 ),
-                const SizedBox(width: 8),
-                GestureDetector(
-                  onTap: _search,
-                  child: Container(
-                    width: 40,
-                    height: 40,
-                    decoration: BoxDecoration(
-                      color: colors.primary,
-                      borderRadius: BorderRadius.circular(20),
-                    ),
-                    child: const Icon(
-                      Icons.search,
-                      color: Colors.white,
-                      size: 22,
-                    ),
-                  ),
-                ),
               ],
               ],
             ),
             ),
           ),
           ),
@@ -805,7 +754,7 @@ class _ExpenseApplyImportPageState extends ConsumerState<ExpenseApplyImportPage>
               ),
               ),
             ),
             ),
             Text(
             Text(
-              '¥${d.amtnYj.toStringAsFixed(2)}',
+              formatAmount(d.amtnYj),
               style: TextStyle(
               style: TextStyle(
                 fontSize: 15,
                 fontSize: 15,
                 fontWeight: FontWeight.w600,
                 fontWeight: FontWeight.w600,

+ 7 - 6
lib/features/expense/expense_create_page.dart

@@ -21,6 +21,7 @@ import 'expense_model.dart';
 import '../../core/theme/app_colors.dart';
 import '../../core/theme/app_colors.dart';
 import '../../core/theme/app_colors_extension.dart';
 import '../../core/theme/app_colors_extension.dart';
 import '../../core/navigation/host_app_channel.dart';
 import '../../core/navigation/host_app_channel.dart';
+import '../../core/utils/amount_utils.dart';
 import 'widgets/expense_detail_dialog.dart';
 import 'widgets/expense_detail_dialog.dart';
 import '../../shared/models/bill_file_rights.dart';
 import '../../shared/models/bill_file_rights.dart';
 import '../../shared/widgets/action_bar.dart';
 import '../../shared/widgets/action_bar.dart';
@@ -699,7 +700,7 @@ class _ExpenseCreatePageState extends ConsumerState<ExpenseCreatePage> {
                                 crossAxisAlignment: CrossAxisAlignment.end,
                                 crossAxisAlignment: CrossAxisAlignment.end,
                                 children: [
                                 children: [
                                   Text(
                                   Text(
-                                    '¥${d.totalAmount.toStringAsFixed(2)}',
+                                    formatAmount(d.totalAmount),
                                     style: TextStyle(
                                     style: TextStyle(
                                       fontSize: AppFontSizes.body,
                                       fontSize: AppFontSizes.body,
                                       fontWeight: FontWeight.w600,
                                       fontWeight: FontWeight.w600,
@@ -708,7 +709,7 @@ class _ExpenseCreatePageState extends ConsumerState<ExpenseCreatePage> {
                                   ),
                                   ),
                                   if (d.approvedAmount > 0)
                                   if (d.approvedAmount > 0)
                                     Text(
                                     Text(
-                                      '¥${d.approvedAmount.toStringAsFixed(2)}',
+                                      formatAmount(d.approvedAmount),
                                       style: TextStyle(
                                       style: TextStyle(
                                         fontSize: AppFontSizes.body,
                                         fontSize: AppFontSizes.body,
                                         fontWeight: FontWeight.w600,
                                         fontWeight: FontWeight.w600,
@@ -721,7 +722,7 @@ class _ExpenseCreatePageState extends ConsumerState<ExpenseCreatePage> {
                           ),
                           ),
                           const SizedBox(height: 2),
                           const SizedBox(height: 2),
                           Text(
                           Text(
-                            '${l10n.get('amountExcludingTax')}: ¥${d.amount.toStringAsFixed(2)}',
+                            '${l10n.get('amountExcludingTax')}: ${formatAmount(d.amount)}',
                             style: TextStyle(
                             style: TextStyle(
                               fontSize: AppFontSizes.caption,
                               fontSize: AppFontSizes.caption,
                               color: colors.textSecondary,
                               color: colors.textSecondary,
@@ -729,7 +730,7 @@ class _ExpenseCreatePageState extends ConsumerState<ExpenseCreatePage> {
                           ),
                           ),
                           if (d.taxAmount > 0)
                           if (d.taxAmount > 0)
                             Text(
                             Text(
-                              '${l10n.get('taxAmount')}: ¥${d.taxAmount.toStringAsFixed(2)}',
+                              '${l10n.get('taxAmount')}: ${formatAmount(d.taxAmount)}',
                               style: TextStyle(
                               style: TextStyle(
                                 fontSize: AppFontSizes.caption,
                                 fontSize: AppFontSizes.caption,
                                 color: colors.textSecondary,
                                 color: colors.textSecondary,
@@ -882,7 +883,7 @@ class _ExpenseCreatePageState extends ConsumerState<ExpenseCreatePage> {
               ),
               ),
             ),
             ),
             Text(
             Text(
-              '¥${state.expense.totalAmount.toStringAsFixed(2)}',
+              formatAmount(state.expense.totalAmount),
               style: TextStyle(
               style: TextStyle(
                 fontSize: AppFontSizes.subtitle,
                 fontSize: AppFontSizes.subtitle,
                 fontWeight: FontWeight.w700,
                 fontWeight: FontWeight.w700,
@@ -904,7 +905,7 @@ class _ExpenseCreatePageState extends ConsumerState<ExpenseCreatePage> {
               ),
               ),
             ),
             ),
             Text(
             Text(
-              '¥${totalApproved.toStringAsFixed(2)}',
+              formatAmount(totalApproved),
               style: TextStyle(
               style: TextStyle(
                 fontSize: AppFontSizes.subtitle,
                 fontSize: AppFontSizes.subtitle,
                 fontWeight: FontWeight.w700,
                 fontWeight: FontWeight.w700,

+ 7 - 6
lib/features/expense/expense_detail_page.dart

@@ -23,6 +23,7 @@ import 'package:path_provider/path_provider.dart';
 import 'package:open_filex/open_filex.dart';
 import 'package:open_filex/open_filex.dart';
 import '../../shared/widgets/attachment_preview_page.dart';
 import '../../shared/widgets/attachment_preview_page.dart';
 import 'widgets/expense_detail_view_dialog.dart';
 import 'widgets/expense_detail_view_dialog.dart';
+import '../../core/utils/amount_utils.dart';
 
 
 class ExpenseDetailPage extends ConsumerStatefulWidget {
 class ExpenseDetailPage extends ConsumerStatefulWidget {
   final String billNo;
   final String billNo;
@@ -427,7 +428,7 @@ class _ExpenseDetailPageState extends ConsumerState<ExpenseDetailPage> {
                                 crossAxisAlignment: CrossAxisAlignment.end,
                                 crossAxisAlignment: CrossAxisAlignment.end,
                                 children: [
                                 children: [
                                   Text(
                                   Text(
-                                    '¥${d.totalAmount.toStringAsFixed(2)}',
+                                    formatAmount(d.totalAmount),
                                     style: TextStyle(
                                     style: TextStyle(
                                       fontSize: AppFontSizes.body,
                                       fontSize: AppFontSizes.body,
                                       fontWeight: FontWeight.w600,
                                       fontWeight: FontWeight.w600,
@@ -436,7 +437,7 @@ class _ExpenseDetailPageState extends ConsumerState<ExpenseDetailPage> {
                                   ),
                                   ),
                                   if (d.approvedAmount > 0)
                                   if (d.approvedAmount > 0)
                                     Text(
                                     Text(
-                                      '¥${d.approvedAmount.toStringAsFixed(2)}',
+                                      formatAmount(d.approvedAmount),
                                       style: TextStyle(
                                       style: TextStyle(
                                         fontSize: AppFontSizes.body,
                                         fontSize: AppFontSizes.body,
                                         fontWeight: FontWeight.w600,
                                         fontWeight: FontWeight.w600,
@@ -449,7 +450,7 @@ class _ExpenseDetailPageState extends ConsumerState<ExpenseDetailPage> {
                           ),
                           ),
                           const SizedBox(height: 2),
                           const SizedBox(height: 2),
                           Text(
                           Text(
-                            '${l10n.get('amountExcludingTax')}: ¥${d.amount.toStringAsFixed(2)}',
+                            '${l10n.get('amountExcludingTax')}: ${formatAmount(d.amount)}',
                             style: TextStyle(
                             style: TextStyle(
                               fontSize: AppFontSizes.caption,
                               fontSize: AppFontSizes.caption,
                               color: colors.textSecondary,
                               color: colors.textSecondary,
@@ -457,7 +458,7 @@ class _ExpenseDetailPageState extends ConsumerState<ExpenseDetailPage> {
                           ),
                           ),
                           if (d.taxAmount > 0)
                           if (d.taxAmount > 0)
                             Text(
                             Text(
-                              '${l10n.get('taxAmount')}: ¥${d.taxAmount.toStringAsFixed(2)}',
+                              '${l10n.get('taxAmount')}: ${formatAmount(d.taxAmount)}',
                               style: TextStyle(
                               style: TextStyle(
                                 fontSize: AppFontSizes.caption,
                                 fontSize: AppFontSizes.caption,
                                 color: colors.textSecondary,
                                 color: colors.textSecondary,
@@ -599,7 +600,7 @@ class _ExpenseDetailPageState extends ConsumerState<ExpenseDetailPage> {
                 ),
                 ),
               ),
               ),
               Text(
               Text(
-                '¥${totalAmount.toStringAsFixed(2)}',
+                formatAmount(totalAmount),
                 style: TextStyle(
                 style: TextStyle(
                   fontSize: AppFontSizes.subtitle,
                   fontSize: AppFontSizes.subtitle,
                   fontWeight: FontWeight.w700,
                   fontWeight: FontWeight.w700,
@@ -621,7 +622,7 @@ class _ExpenseDetailPageState extends ConsumerState<ExpenseDetailPage> {
                 ),
                 ),
               ),
               ),
               Text(
               Text(
-                '¥${totalApproved.toStringAsFixed(2)}',
+                formatAmount(totalApproved),
                 style: TextStyle(
                 style: TextStyle(
                   fontSize: AppFontSizes.subtitle,
                   fontSize: AppFontSizes.subtitle,
                   fontWeight: FontWeight.w700,
                   fontWeight: FontWeight.w700,

+ 7 - 6
lib/features/expense/expense_edit_page.dart

@@ -23,6 +23,7 @@ import 'expense_model.dart';
 import '../../core/theme/app_colors.dart';
 import '../../core/theme/app_colors.dart';
 import '../../core/theme/app_colors_extension.dart';
 import '../../core/theme/app_colors_extension.dart';
 import '../../core/navigation/host_app_channel.dart';
 import '../../core/navigation/host_app_channel.dart';
+import '../../core/utils/amount_utils.dart';
 
 
 /// 费用报销修改页
 /// 费用报销修改页
 ///
 ///
@@ -613,7 +614,7 @@ class _ExpenseEditPageState extends ConsumerState<ExpenseEditPage> {
                                 crossAxisAlignment: CrossAxisAlignment.end,
                                 crossAxisAlignment: CrossAxisAlignment.end,
                                 children: [
                                 children: [
                                   Text(
                                   Text(
-                                    '¥${d.totalAmount.toStringAsFixed(2)}',
+                                    formatAmount(d.totalAmount),
                                     style: TextStyle(
                                     style: TextStyle(
                                       fontSize: AppFontSizes.body,
                                       fontSize: AppFontSizes.body,
                                       fontWeight: FontWeight.w600,
                                       fontWeight: FontWeight.w600,
@@ -622,7 +623,7 @@ class _ExpenseEditPageState extends ConsumerState<ExpenseEditPage> {
                                   ),
                                   ),
                                   if (d.approvedAmount > 0)
                                   if (d.approvedAmount > 0)
                                     Text(
                                     Text(
-                                      '¥${d.approvedAmount.toStringAsFixed(2)}',
+                                      formatAmount(d.approvedAmount),
                                       style: TextStyle(
                                       style: TextStyle(
                                         fontSize: AppFontSizes.body,
                                         fontSize: AppFontSizes.body,
                                         fontWeight: FontWeight.w600,
                                         fontWeight: FontWeight.w600,
@@ -635,7 +636,7 @@ class _ExpenseEditPageState extends ConsumerState<ExpenseEditPage> {
                           ),
                           ),
                           const SizedBox(height: 2),
                           const SizedBox(height: 2),
                           Text(
                           Text(
-                            '${l10n.get('amountExcludingTax')}: ¥${d.amount.toStringAsFixed(2)}',
+                            '${l10n.get('amountExcludingTax')}: ${formatAmount(d.amount)}',
                             style: TextStyle(
                             style: TextStyle(
                               fontSize: AppFontSizes.caption,
                               fontSize: AppFontSizes.caption,
                               color: colors.textSecondary,
                               color: colors.textSecondary,
@@ -643,7 +644,7 @@ class _ExpenseEditPageState extends ConsumerState<ExpenseEditPage> {
                           ),
                           ),
                           if (d.taxAmount > 0)
                           if (d.taxAmount > 0)
                             Text(
                             Text(
-                              '${l10n.get('taxAmount')}: ¥${d.taxAmount.toStringAsFixed(2)}',
+                              '${l10n.get('taxAmount')}: ${formatAmount(d.taxAmount)}',
                               style: TextStyle(
                               style: TextStyle(
                                 fontSize: AppFontSizes.caption,
                                 fontSize: AppFontSizes.caption,
                                 color: colors.textSecondary,
                                 color: colors.textSecondary,
@@ -793,7 +794,7 @@ class _ExpenseEditPageState extends ConsumerState<ExpenseEditPage> {
               ),
               ),
             ),
             ),
             Text(
             Text(
-              '¥${totalAmount.toStringAsFixed(2)}',
+              formatAmount(totalAmount),
               style: TextStyle(
               style: TextStyle(
                 fontSize: AppFontSizes.subtitle,
                 fontSize: AppFontSizes.subtitle,
                 fontWeight: FontWeight.w700,
                 fontWeight: FontWeight.w700,
@@ -815,7 +816,7 @@ class _ExpenseEditPageState extends ConsumerState<ExpenseEditPage> {
               ),
               ),
             ),
             ),
             Text(
             Text(
-              '¥${totalApproved.toStringAsFixed(2)}',
+              formatAmount(totalApproved),
               style: TextStyle(
               style: TextStyle(
                 fontSize: AppFontSizes.subtitle,
                 fontSize: AppFontSizes.subtitle,
                 fontWeight: FontWeight.w700,
                 fontWeight: FontWeight.w700,

+ 55 - 82
lib/features/expense/expense_list_page.dart

@@ -1,4 +1,4 @@
-import 'package:flutter/material.dart';
+import 'package:flutter/material.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
 import 'package:go_router/go_router.dart';
 import 'package:go_router/go_router.dart';
 import 'package:tdesign_flutter/tdesign_flutter.dart';
 import 'package:tdesign_flutter/tdesign_flutter.dart';
@@ -11,6 +11,7 @@ import '../../shared/widgets/empty_state.dart';
 import '../../shared/widgets/app_skeletons.dart';
 import '../../shared/widgets/app_skeletons.dart';
 import '../../shared/widgets/list_footer.dart';
 import '../../shared/widgets/list_footer.dart';
 import '../../core/i18n/app_localizations.dart';
 import '../../core/i18n/app_localizations.dart';
+import '../../core/utils/amount_utils.dart';
 import 'expense_list_controller.dart';
 import 'expense_list_controller.dart';
 import 'expense_model.dart';
 import 'expense_model.dart';
 import 'expense_api.dart';
 import 'expense_api.dart';
@@ -22,7 +23,7 @@ class ExpenseListPage extends ConsumerStatefulWidget {
 }
 }
 
 
 class _ExpenseListPageState extends ConsumerState<ExpenseListPage> {
 class _ExpenseListPageState extends ConsumerState<ExpenseListPage> {
-  final _keywordCtrl = TextEditingController();
+  String _keyword = '';
   final _startDateCtrl = TextEditingController();
   final _startDateCtrl = TextEditingController();
   final _endDateCtrl = TextEditingController();
   final _endDateCtrl = TextEditingController();
   late final EasyRefreshController _refreshCtrl;
   late final EasyRefreshController _refreshCtrl;
@@ -57,7 +58,6 @@ class _ExpenseListPageState extends ConsumerState<ExpenseListPage> {
 
 
   @override
   @override
   void dispose() {
   void dispose() {
-    _keywordCtrl.dispose();
     _startDateCtrl.dispose();
     _startDateCtrl.dispose();
     _endDateCtrl.dispose();
     _endDateCtrl.dispose();
     _refreshCtrl.dispose();
     _refreshCtrl.dispose();
@@ -100,7 +100,7 @@ class _ExpenseListPageState extends ConsumerState<ExpenseListPage> {
   }
   }
 
 
   Future<void> _doRefresh() async {
   Future<void> _doRefresh() async {
-    ref.read(expenseKeywordProvider.notifier).state = _keywordCtrl.text.trim();
+    ref.read(expenseKeywordProvider.notifier).state = _keyword;
     ref
     ref
         .read(expenseDateStartProvider.notifier)
         .read(expenseDateStartProvider.notifier)
         .state = _startDateCtrl.text.isNotEmpty
         .state = _startDateCtrl.text.isNotEmpty
@@ -191,7 +191,7 @@ class _ExpenseListPageState extends ConsumerState<ExpenseListPage> {
                 mainAxisSize: MainAxisSize.min,
                 mainAxisSize: MainAxisSize.min,
                 children: [
                 children: [
                   Padding(
                   Padding(
-                    padding: const EdgeInsets.fromLTRB(12, 8, 12, 0),
+                    padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
                     child: Row(
                     child: Row(
                       children: [
                       children: [
                         Expanded(
                         Expanded(
@@ -232,59 +232,22 @@ class _ExpenseListPageState extends ConsumerState<ExpenseListPage> {
                   ),
                   ),
                   const SizedBox(height: 8),
                   const SizedBox(height: 8),
                   Padding(
                   Padding(
-                    padding: const EdgeInsets.fromLTRB(12, 0, 12, 8),
+                    padding: const EdgeInsets.fromLTRB(0, 0, 12, 8),
                     child: Row(
                     child: Row(
                       children: [
                       children: [
                         Expanded(
                         Expanded(
-                          child: Container(
-                            height: 40,
-                            padding: const EdgeInsets.symmetric(horizontal: 16),
-                            decoration: BoxDecoration(
-                              color: colors.bgSecondaryContainer,
-                              borderRadius: BorderRadius.circular(20),
-                              border: Border.all(
-                                color: tdTheme.componentStrokeColor,
-                              ),
-                            ),
-                            child: Row(
-                              children: [
-                                Expanded(
-                                  child: TextField(
-                                    controller: _keywordCtrl,
-                                    style: TextStyle(
-                                      fontSize: 14,
-                                      color: colors.textPrimary,
-                                    ),
-                                    decoration: InputDecoration(
-                                      hintText: l10n.get('searchExpense'),
-                                      hintStyle: TextStyle(
-                                        fontSize: 14,
-                                        color: colors.textSecondary,
-                                      ),
-                                      border: InputBorder.none,
-                                      isCollapsed: true,
-                                    ),
-                                    onChanged: (_) => setState(() {}),
-                                  ),
-                                ),
-                                if (_keywordCtrl.text.isNotEmpty)
-                                  GestureDetector(
-                                    onTap: () {
-                                      _keywordCtrl.clear();
-                                      setState(() {});
-                                      _applyFilter();
-                                    },
-                                    child: Icon(
-                                      Icons.close,
-                                      size: 18,
-                                      color: colors.textSecondary,
-                                    ),
-                                  ),
-                              ],
-                            ),
+                          child: TDSearchBar(
+                            placeHolder: l10n.get('searchExpense'),
+                            style: TDSearchStyle.round,
+                            onTextChanged: (String text) {
+                              setState(() {
+                                _keyword = text;
+                              });
+                              if (text.isEmpty) _applyFilter();
+                            },
+                            onSubmitted: (_) => _applyFilter(),
                           ),
                           ),
                         ),
                         ),
-                        const SizedBox(width: 8),
                         GestureDetector(
                         GestureDetector(
                           onTap: () {
                           onTap: () {
                             final dir = ref.read(expenseSortDirProvider);
                             final dir = ref.read(expenseSortDirProvider);
@@ -310,23 +273,6 @@ class _ExpenseListPageState extends ConsumerState<ExpenseListPage> {
                             ),
                             ),
                           ),
                           ),
                         ),
                         ),
-                        const SizedBox(width: 8),
-                        GestureDetector(
-                          onTap: _applyFilter,
-                          child: Container(
-                            width: 40,
-                            height: 40,
-                            decoration: BoxDecoration(
-                              color: colors.primary,
-                              borderRadius: BorderRadius.circular(20),
-                            ),
-                            child: const Icon(
-                              Icons.search,
-                              color: Colors.white,
-                              size: 22,
-                            ),
-                          ),
-                        ),
                       ],
                       ],
                     ),
                     ),
                   ),
                   ),
@@ -420,9 +366,9 @@ class _ExpenseListContent extends ConsumerWidget {
               : item.applicantName;
               : item.applicantName;
           final card = ListCard(
           final card = ListCard(
             cardNo: item.expenseNo,
             cardNo: item.expenseNo,
-            amount: '¥${item.totalAmount.toStringAsFixed(2)}',
+            amount: formatAmount(item.totalAmount),
             subAmount: item.totalAmtnSh > 0
             subAmount: item.totalAmtnSh > 0
-                ? '¥${item.totalAmtnSh.toStringAsFixed(2)}'
+                ? formatAmount(item.totalAmtnSh)
                 : null,
                 : null,
             applicant: applicant,
             applicant: applicant,
             description: item.purpose,
             description: item.purpose,
@@ -467,9 +413,9 @@ class _ExpenseListContent extends ConsumerWidget {
             : item.applicantName;
             : item.applicantName;
         final card = ListCard(
         final card = ListCard(
           cardNo: item.expenseNo,
           cardNo: item.expenseNo,
-          amount: '¥${item.totalAmount.toStringAsFixed(2)}',
+          amount: formatAmount(item.totalAmount),
           subAmount: item.totalAmtnSh > 0
           subAmount: item.totalAmtnSh > 0
-              ? '¥${item.totalAmtnSh.toStringAsFixed(2)}'
+              ? formatAmount(item.totalAmtnSh)
               : null,
               : null,
           applicant: applicant,
           applicant: applicant,
           description: item.purpose,
           description: item.purpose,
@@ -492,40 +438,67 @@ class _ExpenseListContent extends ConsumerWidget {
   ) {
   ) {
     final tags = <Widget>[];
     final tags = <Widget>[];
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
-    final isDark = Theme.of(context).brightness == Brightness.dark;
-    final tagBg = isDark ? colors.bgSecondaryContainer : Colors.grey.shade200;
-    final tagFg = isDark ? colors.textSecondary : Colors.grey.shade600;
     if (item.isTransferred == 1) {
     if (item.isTransferred == 1) {
       tags.add(
       tags.add(
         Container(
         Container(
           padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
           padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
           decoration: BoxDecoration(
           decoration: BoxDecoration(
-            color: tagBg,
+            color: colors.primary.withValues(alpha: 0.1),
             borderRadius: BorderRadius.circular(4),
             borderRadius: BorderRadius.circular(4),
           ),
           ),
           child: Text(
           child: Text(
             l10n.get('statusTransferred'),
             l10n.get('statusTransferred'),
-            style: TextStyle(fontSize: 11, color: tagFg),
+            style: TextStyle(fontSize: 11, fontWeight: FontWeight.w500, color: colors.primary),
           ),
           ),
         ),
         ),
       );
       );
-      tags.add(const SizedBox(width: 6));
     }
     }
     if (item.isClosed == 1) {
     if (item.isClosed == 1) {
       tags.add(
       tags.add(
         Container(
         Container(
           padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
           padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
           decoration: BoxDecoration(
           decoration: BoxDecoration(
-            color: tagBg,
+            color: colors.warning.withValues(alpha: 0.1),
             borderRadius: BorderRadius.circular(4),
             borderRadius: BorderRadius.circular(4),
           ),
           ),
           child: Text(
           child: Text(
             l10n.get('statusClosed'),
             l10n.get('statusClosed'),
-            style: TextStyle(fontSize: 11, color: tagFg),
+            style: TextStyle(fontSize: 11, fontWeight: FontWeight.w500, color: colors.warning),
           ),
           ),
         ),
         ),
       );
       );
     }
     }
+    if (item.isTransferred != 1 && item.isClosed != 1) {
+      if (item.isAuditApproved) {
+        tags.add(
+          Container(
+            padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
+            decoration: BoxDecoration(
+              color: colors.success.withValues(alpha: 0.1),
+              borderRadius: BorderRadius.circular(4),
+            ),
+            child: Text(
+              l10n.get('statusApproved'),
+              style: TextStyle(fontSize: 11, fontWeight: FontWeight.w500, color: colors.success),
+            ),
+          ),
+        );
+      } else {
+        tags.add(
+          Container(
+            padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
+            decoration: BoxDecoration(
+              color: colors.statusGray.withValues(alpha: 0.1),
+              borderRadius: BorderRadius.circular(4),
+            ),
+            child: Text(
+              l10n.get('statusPending'),
+              style: TextStyle(fontSize: 11, fontWeight: FontWeight.w500, color: colors.statusGray),
+            ),
+          ),
+        );
+      }
+    }
     if (tags.isEmpty) return const SizedBox.shrink();
     if (tags.isEmpty) return const SizedBox.shrink();
     return Row(mainAxisSize: MainAxisSize.min, children: tags);
     return Row(mainAxisSize: MainAxisSize.min, children: tags);
   }
   }

+ 6 - 0
lib/features/expense/expense_model.dart

@@ -34,6 +34,7 @@ class ExpenseModel {
   final bool isDeleted;
   final bool isDeleted;
   final int isTransferred;
   final int isTransferred;
   final int isClosed;
   final int isClosed;
+  final bool isAuditApproved;
 
 
   // ── 瞬态字段(API 从 ERP 实时查询,非存储) ──
   // ── 瞬态字段(API 从 ERP 实时查询,非存储) ──
   final String currentApproverId;
   final String currentApproverId;
@@ -77,6 +78,7 @@ class ExpenseModel {
     this.isDeleted = false,
     this.isDeleted = false,
     this.isTransferred = 0,
     this.isTransferred = 0,
     this.isClosed = 0,
     this.isClosed = 0,
+    this.isAuditApproved = false,
     this.currentApproverId = '',
     this.currentApproverId = '',
     this.approvalChain = const [],
     this.approvalChain = const [],
     this.approvalRecords = const [],
     this.approvalRecords = const [],
@@ -129,6 +131,7 @@ class ExpenseModel {
       isDeleted: json['isDeleted'] as bool? ?? false,
       isDeleted: json['isDeleted'] as bool? ?? false,
       isTransferred: json['isTransferred'] as int? ?? 0,
       isTransferred: json['isTransferred'] as int? ?? 0,
       isClosed: json['isClosed'] as int? ?? 0,
       isClosed: json['isClosed'] as int? ?? 0,
+      isAuditApproved: (json['isAuditApproved'] as int?) == 1,
       currentApproverId: json['currentApproverId'] as String? ?? '',
       currentApproverId: json['currentApproverId'] as String? ?? '',
       approvalChain:
       approvalChain:
           (json['approvalChain'] as List<dynamic>?)
           (json['approvalChain'] as List<dynamic>?)
@@ -188,6 +191,7 @@ class ExpenseModel {
     'isDeleted': isDeleted,
     'isDeleted': isDeleted,
     'isTransferred': isTransferred,
     'isTransferred': isTransferred,
     'isClosed': isClosed,
     'isClosed': isClosed,
+    'isAuditApproved': isAuditApproved ? 1 : 0,
     'currentApproverId': currentApproverId,
     'currentApproverId': currentApproverId,
     'approvalChain': approvalChain,
     'approvalChain': approvalChain,
     'approvalRecords': approvalRecords.map((r) => r.toJson()).toList(),
     'approvalRecords': approvalRecords.map((r) => r.toJson()).toList(),
@@ -228,6 +232,7 @@ class ExpenseModel {
     bool? isDeleted,
     bool? isDeleted,
     int? isTransferred,
     int? isTransferred,
     int? isClosed,
     int? isClosed,
+    bool? isAuditApproved,
     String? currentApproverId,
     String? currentApproverId,
     List<String>? approvalChain,
     List<String>? approvalChain,
     List<ApprovalRecord>? approvalRecords,
     List<ApprovalRecord>? approvalRecords,
@@ -267,6 +272,7 @@ class ExpenseModel {
       isDeleted: isDeleted ?? this.isDeleted,
       isDeleted: isDeleted ?? this.isDeleted,
       isTransferred: isTransferred ?? this.isTransferred,
       isTransferred: isTransferred ?? this.isTransferred,
       isClosed: isClosed ?? this.isClosed,
       isClosed: isClosed ?? this.isClosed,
+      isAuditApproved: isAuditApproved ?? this.isAuditApproved,
       currentApproverId: currentApproverId ?? this.currentApproverId,
       currentApproverId: currentApproverId ?? this.currentApproverId,
       approvalChain: approvalChain ?? this.approvalChain,
       approvalChain: approvalChain ?? this.approvalChain,
       approvalRecords: approvalRecords ?? this.approvalRecords,
       approvalRecords: approvalRecords ?? this.approvalRecords,

+ 6 - 5
lib/features/expense/widgets/expense_detail_dialog.dart

@@ -6,6 +6,7 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 import '../../../core/i18n/app_localizations.dart';
 import '../../../core/i18n/app_localizations.dart';
 import '../../../core/theme/app_colors.dart';
 import '../../../core/theme/app_colors.dart';
 import '../../../core/theme/app_colors_extension.dart';
 import '../../../core/theme/app_colors_extension.dart';
+import '../../../core/utils/amount_utils.dart';
 import '../../../shared/models/bill_file_rights.dart';
 import '../../../shared/models/bill_file_rights.dart';
 import '../expense_api.dart';
 import '../expense_api.dart';
 import '../../../shared/widgets/attachment_picker.dart';
 import '../../../shared/widgets/attachment_picker.dart';
@@ -192,14 +193,14 @@ class _ExpenseDetailDialogState extends State<ExpenseDetailDialog> {
     );
     );
     _descCtrl = TextEditingController(text: d?.purpose ?? '');
     _descCtrl = TextEditingController(text: d?.purpose ?? '');
     _amountCtrl = TextEditingController(
     _amountCtrl = TextEditingController(
-      text: d != null && d.amount > 0 ? d.amount.toStringAsFixed(2) : '',
+      text: d != null && d.amount > 0 ? formatAmountNumeric(d.amount) : '',
     );
     );
     _selCustomer = (d != null && d.customerVendorId.isNotEmpty)
     _selCustomer = (d != null && d.customerVendorId.isNotEmpty)
         ? CustomerItem(cusNo: '', name: '')
         ? CustomerItem(cusNo: '', name: '')
         : null;
         : null;
     _approvedAmountCtrl = TextEditingController(
     _approvedAmountCtrl = TextEditingController(
       text: d != null && d.approvedAmount > 0
       text: d != null && d.approvedAmount > 0
-          ? d.approvedAmount.toStringAsFixed(2)
+          ? formatAmountNumeric(d.approvedAmount)
           : '',
           : '',
     );
     );
     _remarkCtrl = TextEditingController(text: d?.remark ?? '');
     _remarkCtrl = TextEditingController(text: d?.remark ?? '');
@@ -723,7 +724,7 @@ class _ExpenseDetailDialogState extends State<ExpenseDetailDialog> {
         children: [
         children: [
           Expanded(
           Expanded(
             child: Text(
             child: Text(
-              '${_l10n.get('amountExcludingTax')}: ¥${_amountExclTax.toStringAsFixed(2)}',
+              '${_l10n.get('amountExcludingTax')}: ${formatAmount(_amountExclTax)}',
               style: TextStyle(
               style: TextStyle(
                 fontSize: AppFontSizes.body,
                 fontSize: AppFontSizes.body,
                 color: colors.textSecondary,
                 color: colors.textSecondary,
@@ -731,7 +732,7 @@ class _ExpenseDetailDialogState extends State<ExpenseDetailDialog> {
             ),
             ),
           ),
           ),
           Text(
           Text(
-            '${_l10n.get('taxAmount')}: ¥${_taxAmount.toStringAsFixed(2)}',
+            '${_l10n.get('taxAmount')}: ${formatAmount(_taxAmount)}',
             style: TextStyle(
             style: TextStyle(
               fontSize: AppFontSizes.body,
               fontSize: AppFontSizes.body,
               color: colors.textSecondary,
               color: colors.textSecondary,
@@ -871,7 +872,7 @@ class _ExpenseDetailDialogState extends State<ExpenseDetailDialog> {
   Widget _buildApprovedAmountCard() {
   Widget _buildApprovedAmountCard() {
     if (!widget.canEditApprovedAmount) {
     if (!widget.canEditApprovedAmount) {
       final value = _approvedAmountCtrl.text.isNotEmpty
       final value = _approvedAmountCtrl.text.isNotEmpty
-          ? '¥${double.tryParse(_approvedAmountCtrl.text)?.toStringAsFixed(2) ?? _approvedAmountCtrl.text}'
+          ? formatAmount(double.tryParse(_approvedAmountCtrl.text) ?? 0)
           : '-';
           : '-';
       return _readOnlyCard(
       return _readOnlyCard(
         label: _l10n.get('approvedAmount'),
         label: _l10n.get('approvedAmount'),

+ 5 - 4
lib/features/expense/widgets/expense_detail_view_dialog.dart

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
 import 'package:tdesign_flutter/tdesign_flutter.dart';
 import 'package:tdesign_flutter/tdesign_flutter.dart';
 import '../../../core/i18n/app_localizations.dart';
 import '../../../core/i18n/app_localizations.dart';
 import '../../../core/theme/app_colors_extension.dart';
 import '../../../core/theme/app_colors_extension.dart';
+import '../../../core/utils/amount_utils.dart';
 import '../expense_model.dart';
 import '../expense_model.dart';
 
 
 /// 查看费用报销明细弹窗(只读)。
 /// 查看费用报销明细弹窗(只读)。
@@ -74,12 +75,12 @@ class ExpenseDetailViewDialog extends StatelessWidget {
                   crossAxisAlignment: CrossAxisAlignment.start,
                   crossAxisAlignment: CrossAxisAlignment.start,
                   children: [
                   children: [
                     _row(l10n.get('expenseCategory'), catLabel, colors),
                     _row(l10n.get('expenseCategory'), catLabel, colors),
-                    _row(l10n.get('amountInclTax'), '¥${d.totalAmount.toStringAsFixed(2)}', colors),
+                    _row(l10n.get('amountInclTax'), formatAmount(d.totalAmount), colors),
                     if (d.approvedAmount > 0)
                     if (d.approvedAmount > 0)
-                      _row(l10n.get('approvedAmount'), '¥${d.approvedAmount.toStringAsFixed(2)}', colors),
-                    _row(l10n.get('amountExcludingTax'), '¥${d.amount.toStringAsFixed(2)}', colors),
+                      _row(l10n.get('approvedAmount'), formatAmount(d.approvedAmount), colors),
+                    _row(l10n.get('amountExcludingTax'), formatAmount(d.amount), colors),
                     if (d.taxAmount > 0)
                     if (d.taxAmount > 0)
-                      _row(l10n.get('taxAmount'), '¥${d.taxAmount.toStringAsFixed(2)}', colors),
+                      _row(l10n.get('taxAmount'), formatAmount(d.taxAmount), colors),
                     if (d.taxRate > 0)
                     if (d.taxRate > 0)
                       _row(l10n.get('taxRate'), '${d.taxRate.toStringAsFixed(0)}%', colors),
                       _row(l10n.get('taxRate'), '${d.taxRate.toStringAsFixed(0)}%', colors),
                     _row(l10n.get('acctSubject'), d.acctSubjectId.isNotEmpty ? '${d.acctSubjectId}${d.acctSubjectName.isNotEmpty ? '/${d.acctSubjectName}' : ''}' : '-', colors),
                     _row(l10n.get('acctSubject'), d.acctSubjectId.isNotEmpty ? '${d.acctSubjectId}${d.acctSubjectName.isNotEmpty ? '/${d.acctSubjectName}' : ''}' : '-', colors),

+ 3 - 2
lib/features/expense_apply/expense_apply_create_page.dart

@@ -24,6 +24,7 @@ import '../../core/theme/app_colors_extension.dart';
 import '../../core/constants/enums.dart';
 import '../../core/constants/enums.dart';
 import 'expense_apply_api.dart';
 import 'expense_apply_api.dart';
 import 'widgets/expense_apply_detail_dialog.dart';
 import 'widgets/expense_apply_detail_dialog.dart';
+import '../../core/utils/amount_utils.dart';
 
 
 class ExpenseApplyCreatePage extends ConsumerStatefulWidget {
 class ExpenseApplyCreatePage extends ConsumerStatefulWidget {
   final String? id;
   final String? id;
@@ -593,7 +594,7 @@ class _ExpenseApplyCreatePageState
                               ),
                               ),
                               const SizedBox(width: 12),
                               const SizedBox(width: 12),
                               Text(
                               Text(
-                                '¥${d.estimatedAmount.toStringAsFixed(2)}',
+                                formatAmount(d.estimatedAmount),
                                 style: TextStyle(
                                 style: TextStyle(
                                   fontSize: AppFontSizes.caption,
                                   fontSize: AppFontSizes.caption,
                                   fontWeight: FontWeight.w600,
                                   fontWeight: FontWeight.w600,
@@ -709,7 +710,7 @@ class _ExpenseApplyCreatePageState
                 ),
                 ),
               ),
               ),
               Text(
               Text(
-                '¥${_totalAmount().toStringAsFixed(2)}',
+                formatAmount(_totalAmount()),
                 style: TextStyle(
                 style: TextStyle(
                   fontSize: AppFontSizes.subtitle,
                   fontSize: AppFontSizes.subtitle,
                   fontWeight: FontWeight.w700,
                   fontWeight: FontWeight.w700,

+ 3 - 2
lib/features/expense_apply/expense_apply_detail_page.dart

@@ -23,6 +23,7 @@ import '../../shared/models/bill_attachment.dart';
 import 'expense_apply_api.dart';
 import 'expense_apply_api.dart';
 import '../../core/theme/app_colors.dart';
 import '../../core/theme/app_colors.dart';
 import '../../core/theme/app_colors_extension.dart';
 import '../../core/theme/app_colors_extension.dart';
+import '../../core/utils/amount_utils.dart';
 
 
 class ExpenseApplyDetailPage extends ConsumerStatefulWidget {
 class ExpenseApplyDetailPage extends ConsumerStatefulWidget {
   final String billNo;
   final String billNo;
@@ -403,7 +404,7 @@ class _ExpenseApplyDetailPageState
                               ),
                               ),
                               const SizedBox(width: 12),
                               const SizedBox(width: 12),
                               Text(
                               Text(
-                                '¥${d.estimatedAmount.toStringAsFixed(2)}',
+                                formatAmount(d.estimatedAmount),
                                 style: TextStyle(
                                 style: TextStyle(
                                   fontSize: AppFontSizes.caption,
                                   fontSize: AppFontSizes.caption,
                                   fontWeight: FontWeight.w600,
                                   fontWeight: FontWeight.w600,
@@ -518,7 +519,7 @@ class _ExpenseApplyDetailPageState
                 ),
                 ),
               ),
               ),
               Text(
               Text(
-                '¥${app.details.fold<double>(0, (sum, d) => sum + d.estimatedAmount).toStringAsFixed(2)}',
+                formatAmount(app.details.fold<double>(0, (sum, d) => sum + d.estimatedAmount)),
                 style: TextStyle(
                 style: TextStyle(
                   fontSize: AppFontSizes.subtitle,
                   fontSize: AppFontSizes.subtitle,
                   fontWeight: FontWeight.w700,
                   fontWeight: FontWeight.w700,

+ 3 - 2
lib/features/expense_apply/expense_apply_edit_page.dart

@@ -21,6 +21,7 @@ import '../../core/constants/enums.dart';
 import 'expense_apply_api.dart';
 import 'expense_apply_api.dart';
 import 'widgets/expense_apply_detail_dialog.dart';
 import 'widgets/expense_apply_detail_dialog.dart';
 import '../../shared/widgets/searchable_picker_sheet.dart';
 import '../../shared/widgets/searchable_picker_sheet.dart';
+import '../../core/utils/amount_utils.dart';
 
 
 class ExpenseApplyEditPage extends ConsumerStatefulWidget {
 class ExpenseApplyEditPage extends ConsumerStatefulWidget {
   final String billNo;
   final String billNo;
@@ -516,7 +517,7 @@ class _ExpenseApplyEditPageState extends ConsumerState<ExpenseApplyEditPage> {
                               ),
                               ),
                               const SizedBox(width: 12),
                               const SizedBox(width: 12),
                               Text(
                               Text(
-                                '¥${d.estimatedAmount.toStringAsFixed(2)}',
+                                formatAmount(d.estimatedAmount),
                                 style: TextStyle(
                                 style: TextStyle(
                                   fontSize: AppFontSizes.caption,
                                   fontSize: AppFontSizes.caption,
                                   fontWeight: FontWeight.w600,
                                   fontWeight: FontWeight.w600,
@@ -629,7 +630,7 @@ class _ExpenseApplyEditPageState extends ConsumerState<ExpenseApplyEditPage> {
                 ),
                 ),
               ),
               ),
               Text(
               Text(
-                '¥${_totalAmount().toStringAsFixed(2)}',
+                formatAmount(_totalAmount()),
                 style: TextStyle(
                 style: TextStyle(
                   fontSize: AppFontSizes.subtitle,
                   fontSize: AppFontSizes.subtitle,
                   fontWeight: FontWeight.w700,
                   fontWeight: FontWeight.w700,

+ 53 - 25
lib/features/expense_apply/expense_apply_list_page.dart

@@ -1,4 +1,4 @@
-import 'package:flutter/material.dart';
+import 'package:flutter/material.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
 import 'package:go_router/go_router.dart';
 import 'package:go_router/go_router.dart';
 import 'package:tdesign_flutter/tdesign_flutter.dart';
 import 'package:tdesign_flutter/tdesign_flutter.dart';
@@ -14,6 +14,7 @@ import '../../core/i18n/app_localizations.dart';
 import 'expense_apply_list_controller.dart';
 import 'expense_apply_list_controller.dart';
 import 'expense_apply_model.dart';
 import 'expense_apply_model.dart';
 import 'expense_apply_approval_api.dart';
 import 'expense_apply_approval_api.dart';
+import '../../core/utils/amount_utils.dart';
 class ExpenseApplyListPage extends ConsumerStatefulWidget {
 class ExpenseApplyListPage extends ConsumerStatefulWidget {
   const ExpenseApplyListPage({super.key});
   const ExpenseApplyListPage({super.key});
   @override
   @override
@@ -21,7 +22,7 @@ class ExpenseApplyListPage extends ConsumerStatefulWidget {
 }
 }
 
 
 class _ExpenseApplyListPageState extends ConsumerState<ExpenseApplyListPage> {
 class _ExpenseApplyListPageState extends ConsumerState<ExpenseApplyListPage> {
-  final _keywordCtrl = TextEditingController();
+  String _keyword = '';
   final _startDateCtrl = TextEditingController();
   final _startDateCtrl = TextEditingController();
   final _endDateCtrl = TextEditingController();
   final _endDateCtrl = TextEditingController();
   late final EasyRefreshController _refreshCtrl;
   late final EasyRefreshController _refreshCtrl;
@@ -46,7 +47,6 @@ class _ExpenseApplyListPageState extends ConsumerState<ExpenseApplyListPage> {
 
 
   @override
   @override
   void dispose() {
   void dispose() {
-    _keywordCtrl.dispose();
     _startDateCtrl.dispose();
     _startDateCtrl.dispose();
     _endDateCtrl.dispose();
     _endDateCtrl.dispose();
     _refreshCtrl.dispose();
     _refreshCtrl.dispose();
@@ -76,7 +76,7 @@ class _ExpenseApplyListPageState extends ConsumerState<ExpenseApplyListPage> {
   }
   }
 
 
   Future<void> _doRefresh() async {
   Future<void> _doRefresh() async {
-    ref.read(expenseApplyKeywordProvider.notifier).state = _keywordCtrl.text.trim();
+    ref.read(expenseApplyKeywordProvider.notifier).state = _keyword;
     ref.read(expenseApplyDateStartProvider.notifier).state = _startDateCtrl.text.isNotEmpty ? DateTime.tryParse(_startDateCtrl.text) : null;
     ref.read(expenseApplyDateStartProvider.notifier).state = _startDateCtrl.text.isNotEmpty ? DateTime.tryParse(_startDateCtrl.text) : null;
     ref.read(expenseApplyDateEndProvider.notifier).state = _endDateCtrl.text.isNotEmpty ? DateTime.tryParse(_endDateCtrl.text) : null;
     ref.read(expenseApplyDateEndProvider.notifier).state = _endDateCtrl.text.isNotEmpty ? DateTime.tryParse(_endDateCtrl.text) : null;
     ref.read(expenseApplyRefreshProvider.notifier).state++;
     ref.read(expenseApplyRefreshProvider.notifier).state++;
@@ -121,24 +121,26 @@ class _ExpenseApplyListPageState extends ConsumerState<ExpenseApplyListPage> {
             border: Border(bottom: BorderSide(color: tdTheme.componentStrokeColor)),
             border: Border(bottom: BorderSide(color: tdTheme.componentStrokeColor)),
           ),
           ),
           child: Column(mainAxisSize: MainAxisSize.min, children: [
           child: Column(mainAxisSize: MainAxisSize.min, children: [
-            Padding(padding: const EdgeInsets.fromLTRB(12, 8, 12, 0), child: Row(children: [
+            Padding(padding: const EdgeInsets.fromLTRB(16, 8, 16, 0), child: Row(children: [
               Expanded(child: GestureDetector(behavior: HitTestBehavior.opaque, onTap: () => _pickDate(_startDateCtrl), child: _dateChip(_startDateCtrl, l10n.get('filterStartDate'), tdTheme, colors))),
               Expanded(child: GestureDetector(behavior: HitTestBehavior.opaque, onTap: () => _pickDate(_startDateCtrl), child: _dateChip(_startDateCtrl, l10n.get('filterStartDate'), tdTheme, colors))),
               const SizedBox(width: 8), Text('—', style: TextStyle(fontSize: 14, color: colors.textSecondary)), const SizedBox(width: 8),
               const SizedBox(width: 8), Text('—', style: TextStyle(fontSize: 14, color: colors.textSecondary)), const SizedBox(width: 8),
               Expanded(child: GestureDetector(behavior: HitTestBehavior.opaque, onTap: () => _pickDate(_endDateCtrl), child: _dateChip(_endDateCtrl, l10n.get('filterEndDate'), tdTheme, colors))),
               Expanded(child: GestureDetector(behavior: HitTestBehavior.opaque, onTap: () => _pickDate(_endDateCtrl), child: _dateChip(_endDateCtrl, l10n.get('filterEndDate'), tdTheme, colors))),
             ])),
             ])),
             const SizedBox(height: 8),
             const SizedBox(height: 8),
-            Padding(padding: const EdgeInsets.fromLTRB(12, 0, 12, 8), child: Row(children: [
+            Padding(padding: const EdgeInsets.fromLTRB(0, 0, 12, 8), child: Row(children: [
               Expanded(
               Expanded(
-                child: Container(
-                  height: 40, padding: const EdgeInsets.symmetric(horizontal: 16),
-                  decoration: BoxDecoration(color: colors.bgSecondaryContainer, borderRadius: BorderRadius.circular(20), border: Border.all(color: tdTheme.componentStrokeColor)),
-                  child: Row(children: [
-                    Expanded(child: TextField(controller: _keywordCtrl, style: TextStyle(fontSize: 14, color: colors.textPrimary), decoration: InputDecoration(hintText: l10n.get('searchExpenseApply'), hintStyle: TextStyle(fontSize: 14, color: colors.textSecondary), border: InputBorder.none, isCollapsed: true), onChanged: (_) => setState(() {}))),
-                    if (_keywordCtrl.text.isNotEmpty) GestureDetector(onTap: () { _keywordCtrl.clear(); setState(() {}); _applyFilter(); }, child: Icon(Icons.close, size: 18, color: colors.textSecondary)),
-                  ]),
+                child: TDSearchBar(
+                  placeHolder: l10n.get('searchExpenseApply'),
+                  style: TDSearchStyle.round,
+                  onTextChanged: (String text) {
+                    setState(() {
+                      _keyword = text;
+                    });
+                    if (text.isEmpty) _applyFilter();
+                  },
+                  onSubmitted: (_) => _applyFilter(),
                 ),
                 ),
               ),
               ),
-              const SizedBox(width: 8),
               GestureDetector(
               GestureDetector(
                 onTap: () {
                 onTap: () {
                   final dir = ref.read(expenseApplySortDirProvider);
                   final dir = ref.read(expenseApplySortDirProvider);
@@ -147,8 +149,6 @@ class _ExpenseApplyListPageState extends ConsumerState<ExpenseApplyListPage> {
                 },
                 },
                 child: Container(width: 40, height: 40, decoration: BoxDecoration(color: colors.primary, borderRadius: BorderRadius.circular(20)), child: Center(child: Icon(ref.watch(expenseApplySortDirProvider) == 'ASC' ? Icons.arrow_upward : Icons.arrow_downward, color: Colors.white, size: 20))),
                 child: Container(width: 40, height: 40, decoration: BoxDecoration(color: colors.primary, borderRadius: BorderRadius.circular(20)), child: Center(child: Icon(ref.watch(expenseApplySortDirProvider) == 'ASC' ? Icons.arrow_upward : Icons.arrow_downward, color: Colors.white, size: 20))),
               ),
               ),
-              const SizedBox(width: 8),
-              GestureDetector(onTap: _applyFilter, child: Container(width: 40, height: 40, decoration: BoxDecoration(color: colors.primary, borderRadius: BorderRadius.circular(20)), child: const Icon(Icons.search, color: Colors.white, size: 22))),
             ])),
             ])),
           ]),
           ]),
         ),
         ),
@@ -184,16 +184,13 @@ class _ExpenseApplyListContent extends ConsumerWidget {
   Widget _buildContent(AsyncValue<List<ExpenseApplyModel>> itemsAsync, BuildContext context, WidgetRef ref) {
   Widget _buildContent(AsyncValue<List<ExpenseApplyModel>> itemsAsync, BuildContext context, WidgetRef ref) {
     final l10n = AppLocalizations.of(context);
     final l10n = AppLocalizations.of(context);
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
-    final isDark = Theme.of(context).brightness == Brightness.dark;
-    final tagBg = isDark ? colors.bgSecondaryContainer : Colors.grey.shade200;
-    final tagFg = isDark ? colors.textSecondary : Colors.grey.shade600;
     if (itemsAsync.isReloading) {
     if (itemsAsync.isReloading) {
       final oldItems = itemsAsync.valueOrNull ?? [];
       final oldItems = itemsAsync.valueOrNull ?? [];
       if (oldItems.isEmpty) return ListView(children: [const SizedBox(height: 120), EmptyState(message: l10n.get('noExpenseApplications'))]);
       if (oldItems.isEmpty) return ListView(children: [const SizedBox(height: 120), EmptyState(message: l10n.get('noExpenseApplications'))]);
       return ListView.builder(padding: const EdgeInsets.fromLTRB(16, 16, 16, 24), itemCount: oldItems.length, itemBuilder: (_, i) {
       return ListView.builder(padding: const EdgeInsets.fromLTRB(16, 16, 16, 24), itemCount: oldItems.length, itemBuilder: (_, i) {
         final m = oldItems[i];
         final m = oldItems[i];
         final card = ListCard(
         final card = ListCard(
-          cardNo: m.expenseApplyNo, amount: '¥${m.estimatedAmount.toStringAsFixed(2)}',
+          cardNo: m.expenseApplyNo, amount: formatAmount(m.estimatedAmount),
           applicant: m.deptName.isNotEmpty ? '${m.applicantName} · ${m.deptName}' : m.applicantName,
           applicant: m.deptName.isNotEmpty ? '${m.applicantName} · ${m.deptName}' : m.applicantName,
           description: m.remark.isNotEmpty ? '${m.purpose}\n${l10n.get('remark')}: ${m.remark}' : m.purpose,
           description: m.remark.isNotEmpty ? '${m.purpose}\n${l10n.get('remark')}: ${m.remark}' : m.purpose,
           date: du.DateUtils.formatDate(m.createTime),
           date: du.DateUtils.formatDate(m.createTime),
@@ -201,10 +198,12 @@ class _ExpenseApplyListContent extends ConsumerWidget {
             if (m.isTransferred == 1)
             if (m.isTransferred == 1)
               Container(
               Container(
                 padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
                 padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
-                decoration: BoxDecoration(color: tagBg, borderRadius: BorderRadius.circular(4)),
-                child: Text(l10n.get('statusConvertedToExpense'), style: TextStyle(fontSize: 11, color: tagFg)),
+                decoration: BoxDecoration(color: colors.primary.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(4)),
+                child: Text(l10n.get('statusConvertedToExpense'), style: TextStyle(fontSize: 11, fontWeight: FontWeight.w500, color: colors.primary)),
               ),
               ),
             if (m.isTransferred == 1) const SizedBox(width: 6),
             if (m.isTransferred == 1) const SizedBox(width: 6),
+            if (m.isTransferred != 1) _buildAuditTag(m, l10n, colors),
+            if (m.isTransferred != 1) const SizedBox(width: 6),
             _buildUrgencyChip(m.urgency, l10n, colors),
             _buildUrgencyChip(m.urgency, l10n, colors),
           ]),
           ]),
           onTap: () { context.push('/expense-apply/detail/${m.expenseApplyNo}'); },
           onTap: () { context.push('/expense-apply/detail/${m.expenseApplyNo}'); },
@@ -219,7 +218,7 @@ class _ExpenseApplyListContent extends ConsumerWidget {
       if (i == items.length) return ListFooter(itemCount: items.length);
       if (i == items.length) return ListFooter(itemCount: items.length);
       final m = items[i];
       final m = items[i];
       final card = ListCard(
       final card = ListCard(
-        cardNo: m.expenseApplyNo, amount: '¥${m.estimatedAmount.toStringAsFixed(2)}',
+        cardNo: m.expenseApplyNo, amount: formatAmount(m.estimatedAmount),
         applicant: m.deptName.isNotEmpty ? '${m.applicantName} · ${m.deptName}' : m.applicantName,
         applicant: m.deptName.isNotEmpty ? '${m.applicantName} · ${m.deptName}' : m.applicantName,
         description: m.remark.isNotEmpty ? '${m.purpose}\n${l10n.get('remark')}: ${m.remark}' : m.purpose,
         description: m.remark.isNotEmpty ? '${m.purpose}\n${l10n.get('remark')}: ${m.remark}' : m.purpose,
         date: du.DateUtils.formatDate(m.createTime),
         date: du.DateUtils.formatDate(m.createTime),
@@ -227,10 +226,12 @@ class _ExpenseApplyListContent extends ConsumerWidget {
           if (m.isTransferred == 1)
           if (m.isTransferred == 1)
             Container(
             Container(
               padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
               padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
-              decoration: BoxDecoration(color: tagBg, borderRadius: BorderRadius.circular(4)),
-              child: Text(l10n.get('statusConvertedToExpense'), style: TextStyle(fontSize: 11, color: tagFg)),
+              decoration: BoxDecoration(color: colors.primary.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(4)),
+              child: Text(l10n.get('statusConvertedToExpense'), style: TextStyle(fontSize: 11, fontWeight: FontWeight.w500, color: colors.primary)),
             ),
             ),
           if (m.isTransferred == 1) const SizedBox(width: 6),
           if (m.isTransferred == 1) const SizedBox(width: 6),
+          if (m.isTransferred != 1) _buildAuditTag(m, l10n, colors),
+          if (m.isTransferred != 1) const SizedBox(width: 6),
           _buildUrgencyChip(m.urgency, l10n, colors),
           _buildUrgencyChip(m.urgency, l10n, colors),
         ]),
         ]),
         onTap: () { context.push('/expense-apply/detail/${m.expenseApplyNo}'); },
         onTap: () { context.push('/expense-apply/detail/${m.expenseApplyNo}'); },
@@ -256,4 +257,31 @@ class _ExpenseApplyListContent extends ConsumerWidget {
       child: Text(label, style: TextStyle(fontSize: 11, fontWeight: FontWeight.w500, color: color)),
       child: Text(label, style: TextStyle(fontSize: 11, fontWeight: FontWeight.w500, color: color)),
     );
     );
   }
   }
+
+  Widget _buildAuditTag(ExpenseApplyModel item, AppLocalizations l10n, AppColorsExtension colors) {
+    if (item.isAuditApproved) {
+      return Container(
+        padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
+        decoration: BoxDecoration(
+          color: colors.success.withValues(alpha: 0.1),
+          borderRadius: BorderRadius.circular(4),
+        ),
+        child: Text(
+          l10n.get('statusApproved'),
+          style: TextStyle(fontSize: 11, fontWeight: FontWeight.w500, color: colors.success),
+        ),
+      );
+    }
+    return Container(
+      padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
+      decoration: BoxDecoration(
+        color: colors.statusGray.withValues(alpha: 0.1),
+        borderRadius: BorderRadius.circular(4),
+      ),
+      child: Text(
+        l10n.get('statusPending'),
+        style: TextStyle(fontSize: 11, fontWeight: FontWeight.w500, color: colors.statusGray),
+      ),
+    );
+  }
 }
 }

+ 6 - 0
lib/features/expense_apply/expense_apply_model.dart

@@ -26,6 +26,7 @@ class ExpenseApplyModel {
   final DateTime updateTime;
   final DateTime updateTime;
   final bool isDeleted;
   final bool isDeleted;
   final int isTransferred;
   final int isTransferred;
+  final bool isAuditApproved;
 
 
   // ── 瞬态字段(API 从 ERP 实时查询,非存储) ──
   // ── 瞬态字段(API 从 ERP 实时查询,非存储) ──
   final String currentApproverId;
   final String currentApproverId;
@@ -61,6 +62,7 @@ class ExpenseApplyModel {
     required this.updateTime,
     required this.updateTime,
     this.isDeleted = false,
     this.isDeleted = false,
     this.isTransferred = 0,
     this.isTransferred = 0,
+    this.isAuditApproved = false,
     this.currentApproverId = '',
     this.currentApproverId = '',
     this.approvalChain = const [],
     this.approvalChain = const [],
     this.approvalRecords = const [],
     this.approvalRecords = const [],
@@ -104,6 +106,7 @@ class ExpenseApplyModel {
           : DateTime.now(),
           : DateTime.now(),
       isDeleted: json['isDeleted'] as bool? ?? false,
       isDeleted: json['isDeleted'] as bool? ?? false,
       isTransferred: json['isTransferred'] as int? ?? 0,
       isTransferred: json['isTransferred'] as int? ?? 0,
+      isAuditApproved: (json['isAuditApproved'] as int?) == 1,
       currentApproverId: json['currentApproverId'] as String? ?? '',
       currentApproverId: json['currentApproverId'] as String? ?? '',
       approvalChain:
       approvalChain:
           (json['approvalChain'] as List<dynamic>?)
           (json['approvalChain'] as List<dynamic>?)
@@ -156,6 +159,7 @@ class ExpenseApplyModel {
     'updateTime': updateTime.toIso8601String(),
     'updateTime': updateTime.toIso8601String(),
     'isDeleted': isDeleted,
     'isDeleted': isDeleted,
     'isTransferred': isTransferred,
     'isTransferred': isTransferred,
+    'isAuditApproved': isAuditApproved ? 1 : 0,
     'currentApproverId': currentApproverId,
     'currentApproverId': currentApproverId,
     'approvalChain': approvalChain,
     'approvalChain': approvalChain,
     'approvalRecords': approvalRecords.map((r) => r.toJson()).toList(),
     'approvalRecords': approvalRecords.map((r) => r.toJson()).toList(),
@@ -188,6 +192,7 @@ class ExpenseApplyModel {
     DateTime? updateTime,
     DateTime? updateTime,
     bool? isDeleted,
     bool? isDeleted,
     int? isTransferred,
     int? isTransferred,
+    bool? isAuditApproved,
     String? currentApproverId,
     String? currentApproverId,
     List<String>? approvalChain,
     List<String>? approvalChain,
     List<ApprovalRecord>? approvalRecords,
     List<ApprovalRecord>? approvalRecords,
@@ -219,6 +224,7 @@ class ExpenseApplyModel {
       updateTime: updateTime ?? this.updateTime,
       updateTime: updateTime ?? this.updateTime,
       isDeleted: isDeleted ?? this.isDeleted,
       isDeleted: isDeleted ?? this.isDeleted,
       isTransferred: isTransferred ?? this.isTransferred,
       isTransferred: isTransferred ?? this.isTransferred,
+      isAuditApproved: isAuditApproved ?? this.isAuditApproved,
       currentApproverId: currentApproverId ?? this.currentApproverId,
       currentApproverId: currentApproverId ?? this.currentApproverId,
       approvalChain: approvalChain ?? this.approvalChain,
       approvalChain: approvalChain ?? this.approvalChain,
       approvalRecords: approvalRecords ?? this.approvalRecords,
       approvalRecords: approvalRecords ?? this.approvalRecords,

+ 2 - 1
lib/features/expense_apply/widgets/expense_apply_detail_dialog.dart

@@ -8,6 +8,7 @@ import '../../../core/theme/app_colors.dart';
 import '../../../core/theme/app_colors_extension.dart';
 import '../../../core/theme/app_colors_extension.dart';
 import '../../../shared/widgets/searchable_picker_sheet.dart';
 import '../../../shared/widgets/searchable_picker_sheet.dart';
 import '../expense_apply_api.dart';
 import '../expense_apply_api.dart';
+import '../../../core/utils/amount_utils.dart';
 
 
 /// 费用明细输入数据。
 /// 费用明细输入数据。
 class ExpenseDetailData {
 class ExpenseDetailData {
@@ -155,7 +156,7 @@ class _ExpenseApplyDetailDialogState extends State<ExpenseApplyDetailDialog> {
     if (d != null) {
     if (d != null) {
       _purposeCtrl.text = d.purpose;
       _purposeCtrl.text = d.purpose;
       _amountCtrl.text = d.estimatedAmount > 0
       _amountCtrl.text = d.estimatedAmount > 0
-          ? d.estimatedAmount.toStringAsFixed(2)
+          ? formatAmountNumeric(d.estimatedAmount)
           : '';
           : '';
       _remarkCtrl.text = d.remark;
       _remarkCtrl.text = d.remark;
     }
     }

+ 2 - 1
lib/features/expense_apply/widgets/expense_apply_detail_view_dialog.dart

@@ -4,6 +4,7 @@ import '../../../core/i18n/app_localizations.dart';
 import '../../../core/theme/app_colors_extension.dart';
 import '../../../core/theme/app_colors_extension.dart';
 import '../../../core/utils/date_utils.dart' as du;
 import '../../../core/utils/date_utils.dart' as du;
 import '../expense_apply_model.dart';
 import '../expense_apply_model.dart';
+import '../../../core/utils/amount_utils.dart';
 
 
 /// 查看费用明细弹窗(只读)。
 /// 查看费用明细弹窗(只读)。
 ///
 ///
@@ -90,7 +91,7 @@ class ExpenseApplyDetailViewDialog extends StatelessWidget {
                       _row(l10n.get('expenseCategory'), catLabel, colors),
                       _row(l10n.get('expenseCategory'), catLabel, colors),
                       _row(
                       _row(
                         l10n.get('estimatedAmount'),
                         l10n.get('estimatedAmount'),
-                        '¥${d.estimatedAmount.toStringAsFixed(2)}',
+                        formatAmount(d.estimatedAmount),
                         colors,
                         colors,
                       ),
                       ),
                       _row(
                       _row(

+ 6 - 11
lib/features/home/home_page.dart

@@ -8,6 +8,7 @@ import '../../core/i18n/app_localizations.dart';
 import 'home_controller.dart';
 import 'home_controller.dart';
 import '../../core/theme/app_colors.dart';
 import '../../core/theme/app_colors.dart';
 import '../../core/theme/app_colors_extension.dart';
 import '../../core/theme/app_colors_extension.dart';
+import '../../core/utils/amount_utils.dart';
 
 
 /// 工作台首页
 /// 工作台首页
 ///
 ///
@@ -363,7 +364,7 @@ class HomePage extends ConsumerWidget {
             Expanded(
             Expanded(
               child: _buildStatCard(
               child: _buildStatCard(
                 title: l10n.get('monthlyTotalExpense'),
                 title: l10n.get('monthlyTotalExpense'),
-                value: '¥${_formatAmount(summary.monthlyReimbursement)}',
+                value: formatAmount(summary.monthlyReimbursement),
                 valueColor: colors.amountPrimary,
                 valueColor: colors.amountPrimary,
                 colors: colors,
                 colors: colors,
                 onTap: () => context.push('/expense/list'),
                 onTap: () => context.push('/expense/list'),
@@ -469,7 +470,7 @@ class HomePage extends ConsumerWidget {
             Expanded(
             Expanded(
               child: _buildStatCard(
               child: _buildStatCard(
                 title: l10n.get('deptMonthlyReimbursement'),
                 title: l10n.get('deptMonthlyReimbursement'),
-                value: '¥${_formatAmount(summary.deptMonthlyReimbursement)}',
+                value: formatAmount(summary.deptMonthlyReimbursement),
                 valueColor: colors.amountPrimary,
                 valueColor: colors.amountPrimary,
                 colors: colors,
                 colors: colors,
                 onTap: () => context.push('/expense/list'),
                 onTap: () => context.push('/expense/list'),
@@ -519,7 +520,7 @@ class HomePage extends ConsumerWidget {
             Expanded(
             Expanded(
               child: _buildStatCard(
               child: _buildStatCard(
                 title: l10n.get('paidTotal'),
                 title: l10n.get('paidTotal'),
-                value: '¥${_formatAmount(summary.paidTotal)}',
+                value: formatAmount(summary.paidTotal),
                 valueColor: colors.amountPrimary,
                 valueColor: colors.amountPrimary,
                 colors: colors,
                 colors: colors,
                 valueFontSize: 18,
                 valueFontSize: 18,
@@ -530,7 +531,7 @@ class HomePage extends ConsumerWidget {
             Expanded(
             Expanded(
               child: _buildStatCard(
               child: _buildStatCard(
                 title: l10n.get('pendingPaymentTotal'),
                 title: l10n.get('pendingPaymentTotal'),
-                value: '¥${_formatAmount(summary.pendingPaymentTotal)}',
+                value: formatAmount(summary.pendingPaymentTotal),
                 valueColor: colors.warning,
                 valueColor: colors.warning,
                 colors: colors,
                 colors: colors,
                 valueFontSize: 18,
                 valueFontSize: 18,
@@ -542,7 +543,7 @@ class HomePage extends ConsumerWidget {
         const SizedBox(height: AppSpacing.sm),
         const SizedBox(height: AppSpacing.sm),
         _buildStatCard(
         _buildStatCard(
           title: l10n.get('abnormalReturns'),
           title: l10n.get('abnormalReturns'),
-          value: '¥${_formatAmount(summary.abnormalReturns)}',
+          value: formatAmount(summary.abnormalReturns),
           valueColor: colors.danger,
           valueColor: colors.danger,
           colors: colors,
           colors: colors,
           valueFontSize: 18,
           valueFontSize: 18,
@@ -609,12 +610,6 @@ class HomePage extends ConsumerWidget {
     );
     );
   }
   }
 
 
-  /// 格式化金额:保留两位小数,去除末尾多余的 0
-  String _formatAmount(double amount) {
-    final str = amount.toStringAsFixed(2);
-    // 保留两位小数的标准格式
-    return str;
-  }
 }
 }
 
 
 /// 宫格项数据类
 /// 宫格项数据类

+ 1 - 1
lib/features/messages/message_model.dart

@@ -35,7 +35,7 @@ class MessageModel {
     MessageModel(
     MessageModel(
       id: '1',
       id: '1',
       title: '张三提交了差旅报销,待审批',
       title: '张三提交了差旅报销,待审批',
-      content: '报销金额 ¥2,380.00',
+      content: '报销金额 2,380.00',
       msgType: 'approval_notice',
       msgType: 'approval_notice',
       bizType: 'expense',
       bizType: 'expense',
       bizId: 'exp-001',
       bizId: 'exp-001',

+ 7 - 6
lib/features/report/expense_apply_detail_report_page.dart

@@ -8,6 +8,7 @@ import '../../shared/widgets/app_skeletons.dart';
 import 'package:skeletonizer/skeletonizer.dart';
 import 'package:skeletonizer/skeletonizer.dart';
 import '../expense_apply/expense_apply_api.dart';
 import '../expense_apply/expense_apply_api.dart';
 import '../expense_apply/report_model.dart';
 import '../expense_apply/report_model.dart';
+import '../../core/utils/amount_utils.dart';
 
 
 /// 事前申请明细报表 - 页面22
 /// 事前申请明细报表 - 页面22
 
 
@@ -341,7 +342,7 @@ class _ExpenseApplyDetailReportPageState
               Expanded(
               Expanded(
                 child: _statCard(
                 child: _statCard(
                   l10n.get('statTotalApply'),
                   l10n.get('statTotalApply'),
-                  '¥${summary.totalAmount.toStringAsFixed(2)}',
+                  formatAmount(summary.totalAmount),
                   colors.amountPrimary,
                   colors.amountPrimary,
                 ),
                 ),
               ),
               ),
@@ -349,7 +350,7 @@ class _ExpenseApplyDetailReportPageState
               Expanded(
               Expanded(
                 child: _statCard(
                 child: _statCard(
                   l10n.get('statTransferredAmount'),
                   l10n.get('statTransferredAmount'),
-                  '¥${summary.transferredAmount.toStringAsFixed(2)}',
+                  formatAmount(summary.transferredAmount),
                   colors.textPrimary,
                   colors.textPrimary,
                 ),
                 ),
               ),
               ),
@@ -498,7 +499,7 @@ class _ExpenseApplyDetailReportPageState
               showTitles: true,
               showTitles: true,
               reservedSize: 48,
               reservedSize: 48,
               getTitlesWidget: (v, meta) => Text(
               getTitlesWidget: (v, meta) => Text(
-                '¥${v.toInt()}',
+                formatAmount(v.toDouble()),
                 style: TextStyle(fontSize: 10, color: colors.textPlaceholder),
                 style: TextStyle(fontSize: 10, color: colors.textPlaceholder),
               ),
               ),
             ),
             ),
@@ -539,7 +540,7 @@ class _ExpenseApplyDetailReportPageState
           touchTooltipData: LineTouchTooltipData(
           touchTooltipData: LineTouchTooltipData(
             getTooltipItems: (spots) => spots.map((s) {
             getTooltipItems: (spots) => spots.map((s) {
               return LineTooltipItem(
               return LineTooltipItem(
-                '${monthLabels[s.spotIndex]}\n¥${s.y.toStringAsFixed(0)}',
+                '${monthLabels[s.spotIndex]}\n${formatAmount(s.y)}',
                 const TextStyle(
                 const TextStyle(
                   color: Colors.white,
                   color: Colors.white,
                   fontSize: 12,
                   fontSize: 12,
@@ -646,7 +647,7 @@ class _ExpenseApplyDetailReportPageState
       'billDate': d.billDate != null && d.billDate!.length >= 10
       'billDate': d.billDate != null && d.billDate!.length >= 10
           ? d.billDate!.substring(0, 10)
           ? d.billDate!.substring(0, 10)
           : '-',
           : '-',
-      'amount': d.amount.toStringAsFixed(2),
+      'amount': formatAmount(d.amount),
       'reason': d.reason,
       'reason': d.reason,
       'isTransferred': d.isTransferred ? '1' : '',
       'isTransferred': d.isTransferred ? '1' : '',
     }).toList();
     }).toList();
@@ -701,7 +702,7 @@ class _ExpenseApplyDetailReportPageState
                   cellBuilder: (context, rowIndex) {
                   cellBuilder: (context, rowIndex) {
                     final amount = double.parse(tableData[rowIndex]['amount'] as String);
                     final amount = double.parse(tableData[rowIndex]['amount'] as String);
                     return Text(
                     return Text(
-                      '¥${amount.toStringAsFixed(2)}',
+                      formatAmount(amount),
                       style: TextStyle(
                       style: TextStyle(
                         fontSize: 13,
                         fontSize: 13,
                         color: colors.amountPrimary,
                         color: colors.amountPrimary,

+ 10 - 9
lib/features/report/expense_detail_report_page.dart

@@ -8,6 +8,7 @@ import '../../shared/widgets/app_skeletons.dart';
 import 'package:skeletonizer/skeletonizer.dart';
 import 'package:skeletonizer/skeletonizer.dart';
 import '../expense/expense_api.dart';
 import '../expense/expense_api.dart';
 import '../expense_apply/report_model.dart';
 import '../expense_apply/report_model.dart';
+import '../../core/utils/amount_utils.dart';
 
 
 /// 费用报销明细报表 - 页面23
 /// 费用报销明细报表 - 页面23
 
 
@@ -279,7 +280,7 @@ class _ExpenseDetailReportPageState
               Expanded(
               Expanded(
                 child: _statCard(
                 child: _statCard(
                   l10n.get('statExpenseTotal'),
                   l10n.get('statExpenseTotal'),
-                  '¥${summary.totalAmount.toStringAsFixed(2)}',
+                  formatAmount(summary.totalAmount),
                   colors.amountPrimary,
                   colors.amountPrimary,
                 ),
                 ),
               ),
               ),
@@ -287,7 +288,7 @@ class _ExpenseDetailReportPageState
               Expanded(
               Expanded(
                 child: _statCard(
                 child: _statCard(
                   l10n.get('statTransferredToPmtAmount'),
                   l10n.get('statTransferredToPmtAmount'),
-                  '¥${summary.transferredAmount.toStringAsFixed(2)}',
+                  formatAmount(summary.transferredAmount),
                   colors.textPrimary,
                   colors.textPrimary,
                 ),
                 ),
               ),
               ),
@@ -299,7 +300,7 @@ class _ExpenseDetailReportPageState
               Expanded(
               Expanded(
                 child: _statCard(
                 child: _statCard(
                   l10n.get('statApprovedAmount'),
                   l10n.get('statApprovedAmount'),
-                  '¥${summary.approvedAmount.toStringAsFixed(2)}',
+                  formatAmount(summary.approvedAmount),
                   colors.success,
                   colors.success,
                 ),
                 ),
               ),
               ),
@@ -453,7 +454,7 @@ class _ExpenseDetailReportPageState
               showTitles: true,
               showTitles: true,
               reservedSize: 48,
               reservedSize: 48,
               getTitlesWidget: (v, meta) => Text(
               getTitlesWidget: (v, meta) => Text(
-                '¥${v.toInt()}',
+                formatAmount(v.toDouble()),
                 style: TextStyle(fontSize: 10, color: colors.textPlaceholder),
                 style: TextStyle(fontSize: 10, color: colors.textPlaceholder),
               ),
               ),
             ),
             ),
@@ -491,7 +492,7 @@ class _ExpenseDetailReportPageState
           touchTooltipData: LineTouchTooltipData(
           touchTooltipData: LineTouchTooltipData(
             getTooltipItems: (spots) => spots.map((s) {
             getTooltipItems: (spots) => spots.map((s) {
               return LineTooltipItem(
               return LineTooltipItem(
-                '${monthLabels[s.spotIndex]}\n¥${s.y.toStringAsFixed(0)}',
+                '${monthLabels[s.spotIndex]}\n${formatAmount(s.y)}',
                 const TextStyle(
                 const TextStyle(
                   color: Colors.white,
                   color: Colors.white,
                   fontSize: 12,
                   fontSize: 12,
@@ -703,8 +704,8 @@ class _ExpenseDetailReportPageState
       'billDate': d.billDate != null && d.billDate!.length >= 10
       'billDate': d.billDate != null && d.billDate!.length >= 10
           ? d.billDate!.substring(0, 10)
           ? d.billDate!.substring(0, 10)
           : '-',
           : '-',
-      'amount': d.amount.toStringAsFixed(2),
-      'approvedAmount': d.approvedAmount.toStringAsFixed(2),
+      'amount': formatAmount(d.amount),
+      'approvedAmount': formatAmount(d.approvedAmount),
       'reason': d.reason,
       'reason': d.reason,
       'isTransferred': d.isTransferred ? '1' : '',
       'isTransferred': d.isTransferred ? '1' : '',
     }).toList();
     }).toList();
@@ -759,7 +760,7 @@ class _ExpenseDetailReportPageState
                   cellBuilder: (context, rowIndex) {
                   cellBuilder: (context, rowIndex) {
                     final amount = double.parse(tableData[rowIndex]['amount'] as String);
                     final amount = double.parse(tableData[rowIndex]['amount'] as String);
                     return Text(
                     return Text(
-                      '¥${amount.toStringAsFixed(2)}',
+                      formatAmount(amount),
                       style: TextStyle(
                       style: TextStyle(
                         fontSize: 13,
                         fontSize: 13,
                         color: colors.amountPrimary,
                         color: colors.amountPrimary,
@@ -776,7 +777,7 @@ class _ExpenseDetailReportPageState
                   cellBuilder: (context, rowIndex) {
                   cellBuilder: (context, rowIndex) {
                     final amount = double.parse(tableData[rowIndex]['approvedAmount'] as String);
                     final amount = double.parse(tableData[rowIndex]['approvedAmount'] as String);
                     return Text(
                     return Text(
-                      '¥${amount.toStringAsFixed(2)}',
+                      formatAmount(amount),
                       style: TextStyle(
                       style: TextStyle(
                         fontSize: 13,
                         fontSize: 13,
                         color: colors.amountPrimary,
                         color: colors.amountPrimary,

+ 6 - 5
lib/features/report/vehicle_detail_report_page.dart

@@ -4,6 +4,7 @@ import 'package:fl_chart/fl_chart.dart';
 import '../../core/i18n/app_localizations.dart';
 import '../../core/i18n/app_localizations.dart';
 import '../../core/theme/app_colors_extension.dart';
 import '../../core/theme/app_colors_extension.dart';
 import '../../core/auth/role_provider.dart';
 import '../../core/auth/role_provider.dart';
+import '../../core/utils/amount_utils.dart';
 
 
 /// 用车明细报表 - 页面25
 /// 用车明细报表 - 页面25
 
 
@@ -339,7 +340,7 @@ class _VehicleDetailReportPageState
               Expanded(
               Expanded(
                 child: _statCard(
                 child: _statCard(
                   l10n.get('statTotalCost'),
                   l10n.get('statTotalCost'),
-                  '¥16,450',
+                  formatAmount(16450.0),
                   colors.amountPrimary,
                   colors.amountPrimary,
                 ),
                 ),
               ),
               ),
@@ -505,7 +506,7 @@ class _VehicleDetailReportPageState
               reservedSize: 40,
               reservedSize: 40,
               interval: 500,
               interval: 500,
               getTitlesWidget: (v, meta) => Text(
               getTitlesWidget: (v, meta) => Text(
-                '¥${v.toInt()}',
+                formatAmount(v.toDouble()),
                 style: TextStyle(fontSize: 10, color: colors.textPlaceholder),
                 style: TextStyle(fontSize: 10, color: colors.textPlaceholder),
               ),
               ),
             ),
             ),
@@ -545,7 +546,7 @@ class _VehicleDetailReportPageState
             getTooltipItems: (spots) => spots.map((s) {
             getTooltipItems: (spots) => spots.map((s) {
               final label = s.barIndex == 0
               final label = s.barIndex == 0
                   ? '${s.y.toInt()}次'
                   ? '${s.y.toInt()}次'
-                  : '¥${s.y.toInt()}';
+                  : '${formatAmount(s.y)}';
               return LineTooltipItem(
               return LineTooltipItem(
                 '${_months[s.spotIndex]} $label',
                 '${_months[s.spotIndex]} $label',
                 const TextStyle(
                 const TextStyle(
@@ -621,7 +622,7 @@ class _VehicleDetailReportPageState
               final item = _deptList[group.x.toInt()];
               final item = _deptList[group.x.toInt()];
               final prefix = rodIndex == 0
               final prefix = rodIndex == 0
                   ? '${rod.toY.toInt()}次'
                   ? '${rod.toY.toInt()}次'
-                  : '¥${rod.toY.toInt()}';
+                  : '${formatAmount(rod.toY)}';
               return BarTooltipItem(
               return BarTooltipItem(
                 '${item.name}\n$prefix',
                 '${item.name}\n$prefix',
                 const TextStyle(color: Colors.white, fontSize: 12),
                 const TextStyle(color: Colors.white, fontSize: 12),
@@ -822,7 +823,7 @@ class _VehicleDetailReportPageState
                     const SizedBox(width: 16),
                     const SizedBox(width: 16),
                     Expanded(
                     Expanded(
                       child: Text(
                       child: Text(
-                        '¥${d.cost}',
+                        formatAmount(d.cost.toDouble()),
                         textAlign: TextAlign.right,
                         textAlign: TextAlign.right,
                         style: TextStyle(
                         style: TextStyle(
                           fontSize: 13,
                           fontSize: 13,