chengc 2 viikkoa sitten
vanhempi
commit
5b04c82a68

+ 26 - 9
lib/core/router/app_router.dart

@@ -36,6 +36,7 @@ import '../../features/report/vehicle_detail_report_page.dart';
 import '../../features/report/expense_apply_detail_report_page.dart';
 import '../../features/report/outing_log_detail_report_page.dart';
 import '../../features/admin/admin_permissions_page.dart';
+import 'no_swipe_back_page.dart';
 
 Widget _addButton(BuildContext context, String route) => GestureDetector(
   onTap: () => context.push(route),
@@ -111,8 +112,9 @@ GoRouter createAppRouter() => GoRouter(
     ),
     GoRoute(
       path: '/expense/create',
-      pageBuilder: (context, state) => MaterialPage(
+      pageBuilder: (context, state) => NoSwipeBackPage(
         key: ValueKey(state.uri.toString()),
+        name: state.uri.toString(),
         canPop: false,
         child: AppScaffold(
           navConfig: NavBarConfig(showBack: true),
@@ -140,8 +142,10 @@ GoRouter createAppRouter() => GoRouter(
     ),
     GoRoute(
       path: '/expense/edit/:billNo',
-      pageBuilder: (context, state) => MaterialPage(
+      pageBuilder: (context, state) => NoSwipeBackPage(
         key: ValueKey(state.uri.toString()),
+        name: state.uri.toString(),
+        canPop: false,
         child: AppScaffold(
           navConfig: NavBarConfig(
             showBack: true,
@@ -171,8 +175,10 @@ GoRouter createAppRouter() => GoRouter(
     ),
     GoRoute(
       path: '/overtime-apply/create',
-      pageBuilder: (context, state) => MaterialPage(
+      pageBuilder: (context, state) => NoSwipeBackPage(
         key: ValueKey(state.uri.toString()),
+        name: state.uri.toString(),
+        canPop: false,
         child: AppScaffold(
           navConfig: NavBarConfig(showBack: true),
           body: const OvertimeApplyCreatePage(),
@@ -195,8 +201,10 @@ GoRouter createAppRouter() => GoRouter(
     ),
     GoRoute(
       path: '/overtime-apply/edit/:billNo',
-      pageBuilder: (context, state) => MaterialPage(
+      pageBuilder: (context, state) => NoSwipeBackPage(
         key: ValueKey(state.uri.toString()),
+        name: state.uri.toString(),
+        canPop: false,
         child: AppScaffold(
           navConfig: NavBarConfig(
             showBack: true,
@@ -226,8 +234,10 @@ GoRouter createAppRouter() => GoRouter(
     ),
     GoRoute(
       path: '/vehicle-apply/create',
-      pageBuilder: (context, state) => MaterialPage(
+      pageBuilder: (context, state) => NoSwipeBackPage(
         key: ValueKey(state.uri.toString()),
+        name: state.uri.toString(),
+        canPop: false,
         child: AppScaffold(
           navConfig: NavBarConfig(showBack: true),
           body: const VehicleApplyCreatePage(),
@@ -248,8 +258,10 @@ GoRouter createAppRouter() => GoRouter(
     ),
     GoRoute(
       path: '/vehicle-apply/edit/:billNo',
-      pageBuilder: (context, state) => MaterialPage(
+      pageBuilder: (context, state) => NoSwipeBackPage(
         key: ValueKey(state.uri.toString()),
+        name: state.uri.toString(),
+        canPop: false,
         child: AppScaffold(
           navConfig: NavBarConfig(
             showBack: true,
@@ -279,8 +291,10 @@ GoRouter createAppRouter() => GoRouter(
     ),
     GoRoute(
       path: '/outing-log/create',
-      pageBuilder: (context, state) => MaterialPage(
+      pageBuilder: (context, state) => NoSwipeBackPage(
         key: ValueKey(state.uri.toString()),
+        name: state.uri.toString(),
+        canPop: false,
         child: AppScaffold(
           navConfig: NavBarConfig(showBack: true),
           body: const OutingLogCreatePage(),
@@ -374,8 +388,9 @@ GoRouter createAppRouter() => GoRouter(
     ),
     GoRoute(
       path: '/expense-apply/create',
-      pageBuilder: (context, state) => MaterialPage(
+      pageBuilder: (context, state) => NoSwipeBackPage(
         key: ValueKey(state.uri.toString()),
+        name: state.uri.toString(),
         canPop: false,
         child: AppScaffold(
           navConfig: NavBarConfig(showBack: true),
@@ -386,8 +401,10 @@ GoRouter createAppRouter() => GoRouter(
     ),
     GoRoute(
       path: '/expense-apply/edit/:billNo',
-      pageBuilder: (context, state) => MaterialPage(
+      pageBuilder: (context, state) => NoSwipeBackPage(
         key: ValueKey(state.uri.toString()),
+        name: state.uri.toString(),
+        canPop: false,
         child: AppScaffold(
           navConfig: NavBarConfig(
             showBack: true,

+ 33 - 0
lib/core/router/no_swipe_back_page.dart

@@ -0,0 +1,33 @@
+import 'package:flutter/material.dart';
+
+/// 禁用 iOS 左滑返回手势的 [MaterialPage]。
+///
+/// 用于有未保存数据的表单页面(create / edit),强制用户通过
+/// NavBar 返回按钮触发 [PopScope] + [pageBackProvider] 退出检测逻辑。
+///
+/// iOS 左滑手势走 [Navigator.finalizeRoute] 代码路径,不经过
+/// [Navigator.maybePop],因此 [PopScope] 无法拦截。
+/// 通过覆盖 [MaterialPageRoute.popGestureEnabled] 返回 `false`
+/// 彻底禁用 iOS 左滑。
+class NoSwipeBackPage<T> extends MaterialPage<T> {
+  const NoSwipeBackPage({
+    required super.child,
+    required super.key,
+    required super.name,
+    super.arguments,
+    super.canPop,
+  });
+
+  @override
+  Route<T> createRoute(BuildContext context) {
+    return _NoSwipeBackPageRoute<T>(this);
+  }
+}
+
+class _NoSwipeBackPageRoute<T> extends MaterialPageRoute<T> {
+  _NoSwipeBackPageRoute(NoSwipeBackPage<T> page)
+    : super(builder: (_) => page.child, settings: page);
+
+  @override
+  bool get popGestureEnabled => false;
+}

+ 2 - 0
lib/features/expense/expense_api.dart

@@ -620,7 +620,9 @@ class ExpenseApi {
     final response = await _client.get<Map<String, dynamic>>(
       '/OA/GetExpenseBodyReport',
       queryParameters: {
+        // ignore: use_null_aware_elements
         if (startDate != null) 'startDate': startDate,
+        // ignore: use_null_aware_elements
         if (endDate != null) 'endDate': endDate,
         'page': page,
         'size': size,

+ 2 - 1
lib/features/expense/expense_apply_import_page.dart

@@ -207,11 +207,12 @@ class _ExpenseApplyImportPageState extends ConsumerState<ExpenseApplyImportPage>
         _hasMore = list.length >= 20;
       });
     } catch (_) {
-      if (mounted)
+      if (mounted) {
         setState(() {
           _loading = false;
           _filterLoading = false;
         });
+      }
     }
   }
 

+ 15 - 24
lib/features/expense/expense_detail_page.dart

@@ -2,14 +2,12 @@ import 'dart:typed_data';
 import 'package:flutter/material.dart';
 import 'package:tdesign_flutter/tdesign_flutter.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
-import 'package:go_router/go_router.dart';
 import '../../shared/widgets/loading_dialog.dart';
 import '../../core/utils/date_utils.dart' as du;
 import '../../shared/widgets/form_section.dart';
 import '../../shared/widgets/form_field_row.dart';
 import '../../shared/widgets/app_skeletons.dart';
 import '../../shared/widgets/attachment_download_helper.dart';
-import '../../shared/widgets/action_bar.dart';
 import '../../core/navigation/host_app_channel.dart';
 import 'expense_model.dart';
 import '../../core/i18n/app_localizations.dart';
@@ -42,8 +40,6 @@ class _ExpenseDetailPageState extends ConsumerState<ExpenseDetailPage> {
   bool _isLoading = true;
   String? _error;
   Map<String, dynamic>? _billStatus;
-  bool _statusLoading = false;
-  bool _canEdit = false;
 
   @override
   void initState() {
@@ -89,18 +85,13 @@ class _ExpenseDetailPageState extends ConsumerState<ExpenseDetailPage> {
       }
 
       // 3. 获取单据状态(非致命)
-      setState(() => _statusLoading = true);
       try {
         final status = await api.getBillStatus(widget.billNo);
         if (mounted) {
-          setState(() {
-            _billStatus = status;
-            _canEdit = status['canEdit'] == true;
-            _statusLoading = false;
-          });
+          setState(() => _billStatus = status);
         }
       } catch (_) {
-        if (mounted) setState(() => _statusLoading = false);
+        // 获取单据状态失败,忽略
       }
     } catch (e) {
       setState(() => _error = e.toString());
@@ -167,19 +158,19 @@ class _ExpenseDetailPageState extends ConsumerState<ExpenseDetailPage> {
             ),
           ),
         ),
-        // TODO: 等 ERP 提供"能否修改单据"接口后,恢复为 if (_canEdit)
-        if (false)
-          ActionBar(
-            showLeft: false,
-            showCenter: false,
-            rightLabel: l10n.get('editExpense'),
-            onRightTap: () async {
-              final result = await GoRouter.of(
-                context,
-              ).push('/expense/edit/${widget.billNo}');
-              if (result == true && mounted) _loadData();
-            },
-          ),
+        // TODO: 等 ERP 提供"能否修改单据"接口后,恢复编辑按钮
+        // if (_canEdit)
+        //   ActionBar(
+        //     showLeft: false,
+        //     showCenter: false,
+        //     rightLabel: l10n.get('editExpense'),
+        //     onRightTap: () async {
+        //       final result = await GoRouter.of(
+        //         context,
+        //       ).push('/expense/edit/${widget.billNo}');
+        //       if (result == true && mounted) _loadData();
+        //     },
+        //   ),
       ],
     );
   }

+ 1 - 1
lib/features/expense/expense_edit_page.dart

@@ -253,7 +253,7 @@ class _ExpenseEditPageState extends ConsumerState<ExpenseEditPage> {
 
   List<Map<String, dynamic>> _convertAcctTree(dynamic tree) {
     if (tree is! List) return [];
-    return (tree as List).map<Map<String, dynamic>>((item) {
+    return tree.map<Map<String, dynamic>>((item) {
       final map = Map<String, dynamic>.from(item is Map ? item : {});
       if (map.containsKey('children') && map['children'] != null) {
         map['children'] = _convertAcctTree(map['children']);

+ 2 - 0
lib/features/expense_apply/expense_apply_api.dart

@@ -447,7 +447,9 @@ class ExpenseApplyApi {
     final response = await _client.get<Map<String, dynamic>>(
       '/OA/GetExpenseApplyBodyReport',
       queryParameters: {
+        // ignore: use_null_aware_elements
         if (startDate != null) 'startDate': startDate,
+        // ignore: use_null_aware_elements
         if (endDate != null) 'endDate': endDate,
         'page': page,
         'size': size,

+ 15 - 24
lib/features/expense_apply/expense_apply_detail_page.dart

@@ -3,7 +3,6 @@ import 'dart:io';
 import 'package:flutter/material.dart';
 import 'package:path_provider/path_provider.dart';
 import 'package:open_filex/open_filex.dart';
-import 'package:go_router/go_router.dart';
 import '../../shared/widgets/attachment_preview_page.dart';
 import 'package:tdesign_flutter/tdesign_flutter.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
@@ -14,7 +13,6 @@ import '../../shared/widgets/form_field_row.dart';
 import '../../shared/widgets/app_skeletons.dart';
 import '../../shared/models/bill_file_rights.dart';
 import '../../shared/widgets/attachment_download_helper.dart';
-import '../../shared/widgets/action_bar.dart';
 import '../../core/navigation/host_app_channel.dart';
 import 'expense_apply_model.dart';
 import 'widgets/expense_apply_detail_view_dialog.dart';
@@ -48,8 +46,6 @@ class _ExpenseApplyDetailPageState
   bool _attachAvailable = false;
   BillFileRights _billFileRights = BillFileRights.none;
   Map<String, dynamic>? _billStatus;
-  bool _statusLoading = false;
-  bool _canEdit = false;
 
   @override
   void initState() {
@@ -111,18 +107,13 @@ class _ExpenseApplyDetailPageState
       }
 
       // 单据状态(非关键,尽力加载)
-      setState(() => _statusLoading = true);
       try {
         final status = await api.getBillStatus(widget.billNo);
         if (mounted) {
-          setState(() {
-            _billStatus = status;
-            _canEdit = status['canEdit'] == true;
-            _statusLoading = false;
-          });
+          setState(() => _billStatus = status);
         }
       } catch (_) {
-        if (mounted) setState(() => _statusLoading = false);
+        // 获取单据状态失败,忽略
       }
     } catch (e) {
       if (mounted) {
@@ -193,19 +184,19 @@ class _ExpenseApplyDetailPageState
             ),
           ),
         ),
-        // TODO: 等 ERP 提供"能否修改单据"接口后,恢复为 if (_canEdit)
-        if (false)
-          ActionBar(
-            showLeft: false,
-            showCenter: false,
-            rightLabel: l10n.get('editApply'),
-            onRightTap: () async {
-              final result = await GoRouter.of(
-                context,
-              ).push('/expense-apply/edit/${widget.billNo}');
-              if (result == true && mounted) _loadData();
-            },
-          ),
+        // TODO: 等 ERP 提供"能否修改单据"接口后,恢复编辑按钮
+        // if (_canEdit)
+        //   ActionBar(
+        //     showLeft: false,
+        //     showCenter: false,
+        //     rightLabel: l10n.get('editApply'),
+        //     onRightTap: () async {
+        //       final result = await GoRouter.of(
+        //         context,
+        //       ).push('/expense-apply/edit/${widget.billNo}');
+        //       if (result == true && mounted) _loadData();
+        //     },
+        //   ),
       ],
     );
   }

+ 8 - 4
lib/features/expense_apply/expense_apply_list_page.dart

@@ -294,13 +294,14 @@ class _ExpenseApplyListContent extends ConsumerWidget {
     debugPrint(
       '[TBOSS_OA][ExpenseApply] Content build: isLoading=${itemsAsync.isLoading}, hasValue=${itemsAsync.hasValue}, isReloading=${itemsAsync.isReloading}',
     );
-    if (itemsAsync.isLoading && !itemsAsync.hasValue)
+    if (itemsAsync.isLoading && !itemsAsync.hasValue) {
       return Center(
         child: ConstrainedBox(
           constraints: BoxConstraints(maxWidth: r.listMaxWidth),
           child: const SkeletonLoadingList(),
         ),
       );
+    }
     return Center(
       child: ConstrainedBox(
         constraints: BoxConstraints(maxWidth: r.listMaxWidth),
@@ -323,13 +324,14 @@ class _ExpenseApplyListContent extends ConsumerWidget {
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
     if (itemsAsync.isReloading) {
       final oldItems = itemsAsync.valueOrNull ?? [];
-      if (oldItems.isEmpty)
+      if (oldItems.isEmpty) {
         return ListView(
           children: [
             const SizedBox(height: 120),
             EmptyState(message: l10n.get('noExpenseApplications')),
           ],
         );
+      }
       return ListView.builder(
         controller: scrollCtrl,
         padding: const EdgeInsets.fromLTRB(16, 16, 16, 24),
@@ -385,21 +387,23 @@ class _ExpenseApplyListContent extends ConsumerWidget {
         },
       );
     }
-    if (itemsAsync.hasError)
+    if (itemsAsync.hasError) {
       return ListView(
         children: [
           const SizedBox(height: 120),
           EmptyState(message: l10n.get('loadFailed')),
         ],
       );
+    }
     final items = itemsAsync.requireValue;
-    if (items.isEmpty)
+    if (items.isEmpty) {
       return ListView(
         children: [
           const SizedBox(height: 120),
           EmptyState(message: l10n.get('noExpenseApplications')),
         ],
       );
+    }
     return ListView.builder(
       controller: scrollCtrl,
       padding: const EdgeInsets.fromLTRB(16, 16, 16, 24),

+ 2 - 1
lib/features/report/expense_apply_detail_report_page.dart

@@ -154,8 +154,9 @@ class _ExpenseApplyDetailReportPageState
 
   void _onScroll() {
     if (_scrollCtrl.position.pixels <
-        _scrollCtrl.position.maxScrollExtent - 200)
+        _scrollCtrl.position.maxScrollExtent - 200) {
       return;
+    }
     if (_detailTotal > 0 &&
         _details.length < _detailTotal &&
         !_detailLoadingMore) {

+ 6 - 3
lib/features/report/expense_detail_report_page.dart

@@ -157,8 +157,9 @@ class _ExpenseDetailReportPageState
 
   void _onScroll() {
     if (_scrollCtrl.position.pixels <
-        _scrollCtrl.position.maxScrollExtent - 200)
+        _scrollCtrl.position.maxScrollExtent - 200) {
       return;
+    }
     if (_detailTotal > 0 &&
         _details.length < _detailTotal &&
         !_detailLoadingMore) {
@@ -175,11 +176,12 @@ class _ExpenseDetailReportPageState
         startDate: _startCtrl.text.isNotEmpty ? _startCtrl.text : null,
         endDate: _endCtrl.text.isNotEmpty ? _endCtrl.text : null,
       );
-      if (mounted)
+      if (mounted) {
         setState(() {
           _subordinateData = data;
           _subordinateLoading = false;
         });
+      }
     } catch (_) {
       if (mounted) setState(() => _subordinateLoading = false);
     }
@@ -697,8 +699,9 @@ class _ExpenseDetailReportPageState
                               reservedSize: 40,
                               getTitlesWidget: (v, _) {
                                 final i = v.toInt();
-                                if (i < 0 || i >= _subordinateData.length)
+                                if (i < 0 || i >= _subordinateData.length) {
                                   return const SizedBox();
+                                }
                                 return Padding(
                                   padding: const EdgeInsets.only(top: 4),
                                   child: Text(

+ 2 - 2
lib/features/report/vehicle_detail_report_page.dart

@@ -575,7 +575,7 @@ class _VehicleDetailReportPageState
             getTooltipItems: (spots) => spots.map((s) {
               final label = s.barIndex == 0
                   ? '${s.y.toInt()}次'
-                  : '${formatAmount(s.y)}';
+                  : formatAmount(s.y);
               return LineTooltipItem(
                 '${_months[s.spotIndex]} $label',
                 const TextStyle(
@@ -651,7 +651,7 @@ class _VehicleDetailReportPageState
               final item = _deptList[group.x.toInt()];
               final prefix = rodIndex == 0
                   ? '${rod.toY.toInt()}次'
-                  : '${formatAmount(rod.toY)}';
+                  : formatAmount(rod.toY);
               return BarTooltipItem(
                 '${item.name}\n$prefix',
                 const TextStyle(color: Colors.white, fontSize: 12),

+ 1 - 1
lib/shared/widgets/app_input_dialog.dart

@@ -36,7 +36,7 @@ class AppInputDialog {
         keyboardType = const TextInputType.numberWithOptions(decimal: true);
         inputFormatters = [
           FilteringTextInputFormatter.allow(
-            RegExp(r'^-?\d*\.?\d{0,' + decimalPlaces.toString() + '}'),
+            RegExp(r'^-?\d*\.?\d{0,$decimalPlaces}'),
           ),
         ];
     }

+ 1 - 1
lib/shared/widgets/form_section.dart

@@ -59,7 +59,7 @@ class FormSection extends StatelessWidget {
                   ),
                 ],
               ),
-              if (trailing != null) trailing!, // ignore: prefer_null_aware_operator
+              ?trailing,
               if (showAction)
                 GestureDetector(
                   onTap: onActionTap,