浏览代码

refactor: Material 3 with hardcoded mock data, split expense/report on home page

chengc 2 周之前
父节点
当前提交
d366dbf972
共有 26 个文件被更改,包括 2367 次插入51 次删除
  1. 2 2
      lib/core/network/api_response.dart
  2. 34 0
      lib/core/network/mock_data.dart
  3. 14 0
      lib/core/network/mock_interceptor.dart
  4. 39 0
      lib/core/router/app_router.dart
  5. 16 2
      lib/features/announcement/announcement_detail_page.dart
  6. 48 4
      lib/features/announcement/announcement_list_controller.dart
  7. 49 2
      lib/features/expense/expense_detail_page.dart
  8. 157 4
      lib/features/expense/expense_list_controller.dart
  9. 50 0
      lib/features/expense_application/expense_application_api.dart
  10. 247 0
      lib/features/expense_application/expense_application_apply_page.dart
  11. 121 0
      lib/features/expense_application/expense_application_detail_page.dart
  12. 45 0
      lib/features/expense_application/expense_application_list_controller.dart
  13. 163 0
      lib/features/expense_application/expense_application_list_page.dart
  14. 174 0
      lib/features/expense_application/expense_application_model.dart
  15. 13 18
      lib/features/home/home_controller.dart
  16. 45 1
      lib/features/home/home_page.dart
  17. 21 2
      lib/features/outing_log/outing_log_detail_page.dart
  18. 51 4
      lib/features/outing_log/outing_log_list_controller.dart
  19. 33 2
      lib/features/overtime/overtime_detail_page.dart
  20. 102 4
      lib/features/overtime/overtime_list_controller.dart
  21. 224 0
      lib/features/report/expense_apply_detail_report_page.dart
  22. 197 0
      lib/features/report/expense_detail_report_page.dart
  23. 211 0
      lib/features/report/overtime_detail_report_page.dart
  24. 192 0
      lib/features/report/vehicle_detail_report_page.dart
  25. 38 2
      lib/features/vehicle/vehicle_detail_page.dart
  26. 81 4
      lib/features/vehicle/vehicle_list_controller.dart

+ 2 - 2
lib/core/network/api_response.dart

@@ -14,8 +14,8 @@ class ApiResponse<T> {
     return ApiResponse(
       code: json['code'] as int,
       message: json['message'] as String,
-      data: json['data'] != null && fromJsonT != null
-          ? fromJsonT(json['data'])
+      data: json['data'] != null
+          ? (fromJsonT != null ? fromJsonT(json['data']) : json['data'] as T)
           : null,
     );
   }

+ 34 - 0
lib/core/network/mock_data.dart

@@ -14,6 +14,7 @@ class MockData {
           'logCount': 12,
           'announcementCount': 5,
           'announcementUnread': 2,
+          'expenseApplyPending': 1,
           'totalCount': 28,
         },
       };
@@ -92,6 +93,24 @@ class MockData {
         },
       };
 
+  // ==================== 报销申请 ====================
+
+  static List<Map<String, dynamic>> get expenseApplicationList => [
+        _expenseApplication('EA001', 'BXSQ-20240501-001', '张三', '销售部',
+            '差旅费', 3500.0, '上海出差拜访客户', '', 'pending', 'U100',
+            ['U100', 'U200'], '2024-05-01T14:30:00'),
+        _expenseApplication('EA002', 'BXSQ-20240428-002', '李四', '技术部',
+            '办公用品', 1560.0, '采购开发板及配件', '急用', 'approved', '',
+            ['U100', 'U200'], '2024-04-28T09:15:00'),
+      ];
+
+  static Map<String, dynamic> expenseApplicationDetail(String id) => {
+        'code': 0,
+        'message': 'ok',
+        'data': expenseApplicationList.firstWhere((e) => e['id'] == id,
+            orElse: () => expenseApplicationList.first),
+      };
+
   // ==================== 加班 ====================
 
   static List<Map<String, dynamic>> get overtimeList => [
@@ -293,4 +312,19 @@ class MockData {
       'createTime': start, 'updateTime': start, 'approvalRecords': [],
     };
   }
+
+  static Map<String, dynamic> _expenseApplication(
+      String id, String no, String name, String dept, String type,
+      double amt, String purpose, String remark, String status,
+      String approver, List<String> chain, String time) {
+    return {
+      'id': id, 'applicationNo': no, 'applicantId': 'U001',
+      'applicantName': name, 'deptId': 'D001', 'deptName': dept,
+      'expenseType': type, 'estimatedAmount': amt,
+      'purpose': purpose, 'remark': remark, 'status': status,
+      'currentApproverId': approver, 'approvalChain': chain,
+      'createTime': time, 'updateTime': time,
+      'details': [], 'approvalRecords': [],
+    };
+  }
 }

+ 14 - 0
lib/core/network/mock_interceptor.dart

@@ -55,6 +55,20 @@ class MockInterceptor extends Interceptor {
       return MockData.success;
     }
 
+    if (path == '/expense-apply/list') {
+      var list = MockData.expenseApplicationList;
+      if (status.isNotEmpty) {
+        list = list.where((e) => e['status'] == status).toList();
+      }
+      return _paginate(list, page);
+    }
+    if (path.startsWith('/expense-apply/detail/')) {
+      return MockData.expenseApplicationDetail(path.split('/').last);
+    }
+    if (path == '/expense-apply/submit' || path == '/expense-apply/draft') {
+      return MockData.success;
+    }
+
     if (path == '/overtime/list') {
       var list = MockData.overtimeList;
       if (status.isNotEmpty) {

+ 39 - 0
lib/core/router/app_router.dart

@@ -14,6 +14,13 @@ import '../../features/outing_log/outing_log_create_page.dart';
 import '../../features/outing_log/outing_log_detail_page.dart';
 import '../../features/announcement/announcement_list_page.dart';
 import '../../features/announcement/announcement_detail_page.dart';
+import '../../features/expense_application/expense_application_list_page.dart';
+import '../../features/expense_application/expense_application_detail_page.dart';
+import '../../features/expense_application/expense_application_apply_page.dart';
+import '../../features/report/expense_detail_report_page.dart';
+import '../../features/report/overtime_detail_report_page.dart';
+import '../../features/report/vehicle_detail_report_page.dart';
+import '../../features/report/expense_apply_detail_report_page.dart';
 
 GoRouter createAppRouter() {
   return GoRouter(
@@ -89,6 +96,38 @@ GoRouter createAppRouter() {
         builder: (_, state) =>
             AnnouncementDetailPage(id: state.pathParameters['id']!),
       ),
+
+      GoRoute(
+        path: '/expense-apply/list',
+        builder: (_, __) => const ExpenseApplicationListPage(),
+      ),
+      GoRoute(
+        path: '/expense-apply/detail/:id',
+        builder: (_, state) => ExpenseApplicationDetailPage(
+            id: state.pathParameters['id']!),
+      ),
+      GoRoute(
+        path: '/expense-apply/apply',
+        builder: (_, state) => ExpenseApplicationApplyPage(
+            id: state.uri.queryParameters['id']),
+      ),
+
+      GoRoute(
+        path: '/report/expense-detail',
+        builder: (_, __) => const ExpenseDetailReportPage(),
+      ),
+      GoRoute(
+        path: '/report/expense-apply-detail',
+        builder: (_, __) => const ExpenseApplyDetailReportPage(),
+      ),
+      GoRoute(
+        path: '/report/overtime-detail',
+        builder: (_, __) => const OvertimeDetailReportPage(),
+      ),
+      GoRoute(
+        path: '/report/vehicle-detail',
+        builder: (_, __) => const VehicleDetailReportPage(),
+      ),
     ],
   );
 }

+ 16 - 2
lib/features/announcement/announcement_detail_page.dart

@@ -5,11 +5,25 @@ import '../../core/utils/date_utils.dart' as du;
 import '../../core/utils/responsive.dart';
 import '../../shared/widgets/form_section.dart';
 import '../../shared/widgets/form_field_row.dart';
-import 'announcement_api.dart';
 import 'announcement_model.dart';
 
 final announcementDetailProvider = FutureProvider.autoDispose.family<AnnouncementModel, String>((ref, id) async {
-  return ref.read(announcementApiProvider).fetchDetail(id);
+  await Future.delayed(const Duration(milliseconds: 300));
+  return AnnouncementModel(
+    id: id,
+    title: '关于2026年端午节放假安排的通知',
+    content: '根据国务院办公厅通知精神,现将2026年端午节放假安排通知如下:6月25日(星期四)至6月27日(星期六)放假调休,共3天。6月28日(星期日)上班。请各部门提前做好工作安排,确保节日期间各项工作正常运转。',
+    type: '通知公告',
+    publisherId: 'u-admin',
+    publisherName: '行政管理部',
+    publishTime: DateTime(2026, 5, 22),
+    isTop: true,
+    requireConfirm: false,
+    expiryDate: DateTime(2026, 6, 28),
+    readCount: 45,
+    unreadCount: 12,
+    createTime: DateTime(2026, 5, 22),
+  );
 });
 
 class AnnouncementDetailPage extends ConsumerWidget {

+ 48 - 4
lib/features/announcement/announcement_list_controller.dart

@@ -1,11 +1,55 @@
 import 'package:flutter_riverpod/flutter_riverpod.dart';
 import 'announcement_model.dart';
-import 'announcement_api.dart';
+
+final _mockAnnouncements = <AnnouncementModel>[
+  AnnouncementModel(
+    id: 'ann-001',
+    title: '关于2026年端午节放假安排的通知',
+    content: '根据国务院办公厅通知精神,现将2026年端午节放假安排通知如下:6月25日(星期四)至6月27日(星期六)放假调休,共3天。6月28日(星期日)上班。请各部门提前做好工作安排,确保节日期间各项工作正常运转。',
+    type: '通知公告',
+    publisherId: 'u-admin',
+    publisherName: '行政管理部',
+    publishTime: DateTime(2026, 5, 22),
+    isTop: true,
+    expiryDate: DateTime(2026, 6, 28),
+    readCount: 45,
+    unreadCount: 12,
+    createTime: DateTime(2026, 5, 22),
+  ),
+  AnnouncementModel(
+    id: 'ann-002',
+    title: '关于启用新版考勤系统的通知',
+    content: '为提升考勤管理效率,公司决定于2026年6月1日起全面启用新版考勤系统。新旧系统切换期间,请各部门配合完成以下事项:1. 5月28日前完成全员信息核对;2. 5月29日-31日进行系统试运行。如有问题请及时联系IT部门。',
+    type: '系统公告',
+    publisherId: 'u-admin',
+    publisherName: '信息技术部',
+    publishTime: DateTime(2026, 5, 20),
+    isTop: false,
+    requireConfirm: true,
+    expiryDate: DateTime(2026, 6, 15),
+    readCount: 30,
+    unreadCount: 27,
+    createTime: DateTime(2026, 5, 20),
+  ),
+  AnnouncementModel(
+    id: 'ann-003',
+    title: '2026年第二季度团建活动报名通知',
+    content: '为增强团队凝聚力,公司将于2026年6月10日组织第二季度团建活动。本次活动地点为北京市怀柔区雁栖湖,活动内容包括户外拓展训练、团队协作游戏和烧烤晚会。请各部门于6月3日前将参加人数报至行政管理部。',
+    type: '活动通知',
+    publisherId: 'u-admin',
+    publisherName: '行政管理部',
+    publishTime: DateTime(2026, 5, 18),
+    isTop: false,
+    expiryDate: DateTime(2026, 6, 10),
+    readCount: 52,
+    unreadCount: 5,
+    createTime: DateTime(2026, 5, 18),
+  ),
+];
 
 final announcementListProvider =
     FutureProvider.autoDispose.family<List<AnnouncementModel>, int>(
         (ref, page) async {
-  final api = ref.watch(announcementApiProvider);
-  final result = await api.fetchList(page: page);
-  return result.list;
+  await Future.delayed(const Duration(milliseconds: 300));
+  return _mockAnnouncements;
 });

+ 49 - 2
lib/features/expense/expense_detail_page.dart

@@ -3,16 +3,63 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
 import '../../core/theme/app_colors.dart';
 import '../../core/utils/date_utils.dart' as du;
 import '../../core/utils/responsive.dart';
+import '../../shared/models/approval_status.dart';
 import '../../shared/widgets/form_section.dart';
 import '../../shared/widgets/form_field_row.dart';
 import '../../shared/widgets/status_tag.dart';
-import 'expense_api.dart';
 import 'expense_model.dart';
 
 final expenseDetailProvider =
     FutureProvider.autoDispose.family<ExpenseModel, String>(
         (ref, id) async {
-  return ref.read(expenseApiProvider).fetchDetail(id);
+  await Future.delayed(const Duration(milliseconds: 300));
+  return ExpenseModel(
+    id: id,
+    reportNo: 'BX202605001',
+    applicantId: 'u-001',
+    applicantName: '张三',
+    deptId: 'dept-001',
+    deptName: '市场部',
+    expenseType: '差旅费',
+    totalAmount: 2580.00,
+    invoiceCount: 3,
+    status: 'pending',
+    createTime: DateTime(2026, 5, 20),
+    updateTime: DateTime(2026, 5, 20),
+    details: [
+      ExpenseDetailModel(
+        id: 'det-001',
+        expenseId: id,
+        expenseDate: DateTime(2026, 5, 19),
+        expenseType: '交通费',
+        expenseDesc: '北京-上海高铁',
+        amount: 553.00,
+        totalAmount: 553.00,
+      ),
+      ExpenseDetailModel(
+        id: 'det-002',
+        expenseId: id,
+        expenseDate: DateTime(2026, 5, 19),
+        expenseType: '住宿费',
+        expenseDesc: '上海酒店住宿',
+        amount: 800.00,
+        totalAmount: 800.00,
+      ),
+    ],
+    approvalRecords: [
+      ApprovalRecord(
+        id: 'ar-001',
+        bizId: id,
+        bizType: 'expense',
+        approverId: 'u-mgr',
+        approverName: '李四',
+        approvalLevel: 1,
+        action: 'pending',
+        opinion: '',
+        approvalTime: DateTime(2026, 5, 21),
+      ),
+    ],
+  );
 });
 
 class ExpenseDetailPage extends ConsumerWidget {

+ 157 - 4
lib/features/expense/expense_list_controller.dart

@@ -1,14 +1,167 @@
 import 'package:flutter_riverpod/flutter_riverpod.dart';
+import '../../shared/models/approval_status.dart';
 import 'expense_model.dart';
-import 'expense_api.dart';
 
 final expenseStatusFilterProvider = StateProvider<String>((ref) => '');
 
+final _mockExpenses = <ExpenseModel>[
+  ExpenseModel(
+    id: 'exp-001',
+    reportNo: 'BX202605001',
+    applicantId: 'u-001',
+    applicantName: '张三',
+    deptId: 'dept-001',
+    deptName: '市场部',
+    expenseType: '差旅费',
+    totalAmount: 2580.00,
+    invoiceCount: 3,
+    status: 'pending',
+    createTime: DateTime(2026, 5, 20),
+    updateTime: DateTime(2026, 5, 20),
+    details: [
+      ExpenseDetailModel(
+        id: 'det-001',
+        expenseId: 'exp-001',
+        expenseDate: DateTime(2026, 5, 19),
+        expenseType: '交通费',
+        expenseDesc: '北京-上海高铁',
+        amount: 553.00,
+        totalAmount: 553.00,
+      ),
+      ExpenseDetailModel(
+        id: 'det-002',
+        expenseId: 'exp-001',
+        expenseDate: DateTime(2026, 5, 19),
+        expenseType: '住宿费',
+        expenseDesc: '上海酒店住宿',
+        amount: 800.00,
+        totalAmount: 800.00,
+      ),
+    ],
+    approvalRecords: [
+      ApprovalRecord(
+        id: 'ar-001',
+        bizId: 'exp-001',
+        bizType: 'expense',
+        approverId: 'u-mgr',
+        approverName: '李四',
+        approvalLevel: 1,
+        action: 'pending',
+        opinion: '',
+        approvalTime: DateTime(2026, 5, 21),
+      ),
+    ],
+  ),
+  ExpenseModel(
+    id: 'exp-002',
+    reportNo: 'BX202605002',
+    applicantId: 'u-002',
+    applicantName: '王五',
+    deptId: 'dept-002',
+    deptName: '技术部',
+    expenseType: '办公用品',
+    totalAmount: 1280.50,
+    invoiceCount: 2,
+    status: 'approved',
+    createTime: DateTime(2026, 5, 15),
+    updateTime: DateTime(2026, 5, 18),
+    details: [
+      ExpenseDetailModel(
+        id: 'det-003',
+        expenseId: 'exp-002',
+        expenseDate: DateTime(2026, 5, 14),
+        expenseType: '办公用品',
+        expenseDesc: '打印纸、墨盒',
+        amount: 1280.50,
+        totalAmount: 1280.50,
+      ),
+    ],
+    approvalRecords: [
+      ApprovalRecord(
+        id: 'ar-002',
+        bizId: 'exp-002',
+        bizType: 'expense',
+        approverId: 'u-mgr',
+        approverName: '李四',
+        approvalLevel: 1,
+        action: 'approve',
+        opinion: '同意',
+        approvalTime: DateTime(2026, 5, 18),
+      ),
+    ],
+  ),
+  ExpenseModel(
+    id: 'exp-003',
+    reportNo: 'BX202605003',
+    applicantId: 'u-003',
+    applicantName: '赵六',
+    deptId: 'dept-003',
+    deptName: '财务部',
+    expenseType: '业务招待费',
+    totalAmount: 3600.00,
+    invoiceCount: 1,
+    status: 'rejected',
+    createTime: DateTime(2026, 5, 10),
+    updateTime: DateTime(2026, 5, 12),
+    details: [
+      ExpenseDetailModel(
+        id: 'det-004',
+        expenseId: 'exp-003',
+        expenseDate: DateTime(2026, 5, 9),
+        expenseType: '招待费',
+        expenseDesc: '客户晚宴',
+        amount: 3600.00,
+        totalAmount: 3600.00,
+      ),
+    ],
+    approvalRecords: [
+      ApprovalRecord(
+        id: 'ar-003',
+        bizId: 'exp-003',
+        bizType: 'expense',
+        approverId: 'u-mgr',
+        approverName: '李四',
+        approvalLevel: 1,
+        action: 'reject',
+        opinion: '发票不符合规定',
+        approvalTime: DateTime(2026, 5, 12),
+      ),
+    ],
+  ),
+  ExpenseModel(
+    id: 'exp-004',
+    reportNo: 'BX202605004',
+    applicantId: 'u-001',
+    applicantName: '张三',
+    deptId: 'dept-001',
+    deptName: '市场部',
+    expenseType: '交通费',
+    totalAmount: 890.00,
+    invoiceCount: 1,
+    status: 'pending',
+    createTime: DateTime(2026, 5, 22),
+    updateTime: DateTime(2026, 5, 22),
+    details: [
+      ExpenseDetailModel(
+        id: 'det-005',
+        expenseId: 'exp-004',
+        expenseDate: DateTime(2026, 5, 21),
+        expenseType: '交通费',
+        expenseDesc: '市内打车',
+        amount: 890.00,
+        totalAmount: 890.00,
+      ),
+    ],
+  ),
+];
+
 final expenseListProvider =
     FutureProvider.autoDispose.family<List<ExpenseModel>, int>(
         (ref, page) async {
+  await Future.delayed(const Duration(milliseconds: 300));
   final status = ref.watch(expenseStatusFilterProvider);
-  final api = ref.watch(expenseApiProvider);
-  final result = await api.fetchList(status: status, page: page);
-  return result.list;
+  if (status.isEmpty) {
+    return _mockExpenses;
+  }
+  return _mockExpenses.where((e) => e.status == status).toList();
 });

+ 50 - 0
lib/features/expense_application/expense_application_api.dart

@@ -0,0 +1,50 @@
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import '../../core/network/api_client.dart';
+import '../../app.dart';
+import '../../shared/models/pagination_model.dart';
+import 'expense_application_model.dart';
+
+final expenseApplicationApiProvider = Provider<ExpenseApplicationApi>(
+    (ref) => ExpenseApplicationApi(ref.read(apiClientProvider)));
+
+class ExpenseApplicationApi {
+  final ApiClient _client;
+  ExpenseApplicationApi(this._client);
+
+  Future<PaginatedData<ExpenseApplicationModel>> fetchList({
+    String status = '',
+    int page = 1,
+    int size = 20,
+  }) async {
+    final response = await _client.get<Map<String, dynamic>>(
+      '/expense-apply/list',
+      queryParameters: {'status': status, 'page': page, 'size': size},
+    );
+    final data = response.data!;
+    final list = (data['list'] as List<dynamic>)
+        .map((e) =>
+            ExpenseApplicationModel.fromJson(e as Map<String, dynamic>))
+        .toList();
+    return PaginatedData(
+      list: list,
+      page: data['page'] as int,
+      size: data['size'] as int,
+      total: data['total'] as int,
+    );
+  }
+
+  Future<ExpenseApplicationModel> fetchDetail(String id) async {
+    final response = await _client.get<Map<String, dynamic>>(
+      '/expense-apply/detail/$id',
+    );
+    return ExpenseApplicationModel.fromJson(response.data!);
+  }
+
+  Future<void> submit(ExpenseApplicationModel model) async {
+    await _client.post('/expense-apply/submit', data: model.toJson());
+  }
+
+  Future<void> saveDraft(ExpenseApplicationModel model) async {
+    await _client.put('/expense-apply/draft', data: model.toJson());
+  }
+}

+ 247 - 0
lib/features/expense_application/expense_application_apply_page.dart

@@ -0,0 +1,247 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:go_router/go_router.dart';
+import '../../core/theme/app_colors.dart';
+import '../../core/utils/responsive.dart';
+import '../../shared/widgets/form_section.dart';
+import '../../shared/widgets/form_field_row.dart';
+
+class ExpenseApplicationApplyPage extends ConsumerStatefulWidget {
+  final String? id;
+  const ExpenseApplicationApplyPage({super.key, this.id});
+
+  @override
+  ConsumerState<ExpenseApplicationApplyPage> createState() =>
+      _ExpenseApplicationApplyPageState();
+}
+
+class _ExpenseApplicationApplyPageState
+    extends ConsumerState<ExpenseApplicationApplyPage> {
+  final _formKey = GlobalKey<FormState>();
+  String _expenseType = '差旅费';
+  final _amountController = TextEditingController();
+  final _purposeController = TextEditingController();
+  final _remarkController = TextEditingController();
+
+  static const _types = ['差旅费', '办公用品', '招待费', '交通费', '通讯费', '其他'];
+
+  @override
+  void dispose() {
+    _amountController.dispose();
+    _purposeController.dispose();
+    _remarkController.dispose();
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    final r = ResponsiveHelper.of(context);
+
+    return Scaffold(
+      appBar: AppBar(title: const Text('报销申请')),
+      body: Column(
+        children: [
+          Expanded(
+            child: Center(
+              child: ConstrainedBox(
+                constraints: BoxConstraints(maxWidth: r.formMaxWidth),
+                child: SingleChildScrollView(
+                  padding: const EdgeInsets.symmetric(vertical: 8),
+                  child: Form(
+                    key: _formKey,
+                    child: _buildForm(),
+                  ),
+                ),
+              ),
+            ),
+          ),
+          _buildBottomButtons(),
+        ],
+      ),
+    );
+  }
+
+  Widget _buildForm() {
+    return FormSection(
+      title: '基本信息',
+      children: [
+        FormFieldRow(
+          label: '费用类型',
+          value: _expenseType,
+          onTap: _showTypePicker,
+        ),
+        Padding(
+          padding: const EdgeInsets.fromLTRB(14, 0, 14, 12),
+          child: Row(
+            children: [
+              const SizedBox(
+                width: 72,
+                child: Text('预计金额',
+                    style: TextStyle(
+                        color: AppColors.textSecondary, fontSize: 13)),
+              ),
+              const SizedBox(width: 8),
+              Expanded(
+                child: TextFormField(
+                  controller: _amountController,
+                  keyboardType:
+                      const TextInputType.numberWithOptions(decimal: true),
+                  decoration: const InputDecoration(
+                    hintText: '请输入金额',
+                    border: OutlineInputBorder(),
+                    contentPadding:
+                        EdgeInsets.symmetric(horizontal: 10, vertical: 8),
+                    isDense: true,
+                  ),
+                  validator: (value) {
+                    if (value == null || value.isEmpty) {
+                      return '请输入预计金额';
+                    }
+                    if (double.tryParse(value) == null) {
+                      return '请输入有效数字';
+                    }
+                    return null;
+                  },
+                ),
+              ),
+            ],
+          ),
+        ),
+        Padding(
+          padding: const EdgeInsets.fromLTRB(14, 0, 14, 12),
+          child: Row(
+            children: [
+              const SizedBox(
+                width: 72,
+                child: Text('用途说明',
+                    style: TextStyle(
+                        color: AppColors.textSecondary, fontSize: 13)),
+              ),
+              const SizedBox(width: 8),
+              Expanded(
+                child: TextFormField(
+                  controller: _purposeController,
+                  maxLines: 2,
+                  decoration: const InputDecoration(
+                    hintText: '请输入用途说明',
+                    border: OutlineInputBorder(),
+                    contentPadding:
+                        EdgeInsets.symmetric(horizontal: 10, vertical: 8),
+                    isDense: true,
+                  ),
+                  validator: (value) {
+                    if (value == null || value.isEmpty) {
+                      return '请输入用途说明';
+                    }
+                    return null;
+                  },
+                ),
+              ),
+            ],
+          ),
+        ),
+        FormFieldRow(
+          label: '备注',
+          value: _remarkController.text.isEmpty ? null : _remarkController.text,
+          hint: '选填',
+          onTap: _showRemarkEditor,
+        ),
+      ],
+    );
+  }
+
+  Widget _buildBottomButtons() {
+    return Container(
+      padding: const EdgeInsets.all(12),
+      decoration: BoxDecoration(
+        color: Colors.white,
+        boxShadow: [
+          BoxShadow(
+            color: Colors.black.withValues(alpha: 0.05),
+            blurRadius: 4,
+            offset: const Offset(0, -1),
+          ),
+        ],
+      ),
+      child: Row(
+        children: [
+          Expanded(
+            child: OutlinedButton(
+              onPressed: _handleSaveDraft,
+              child: const Text('存草稿'),
+            ),
+          ),
+          const SizedBox(width: 12),
+          Expanded(
+            flex: 2,
+            child: ElevatedButton(
+              onPressed: _handleSubmit,
+              child: const Text('提交申请'),
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+
+  void _showTypePicker() {
+    showModalBottomSheet(
+      context: context,
+      builder: (_) => Column(
+        mainAxisSize: MainAxisSize.min,
+        children: _types
+            .map((t) => ListTile(
+                  title: Text(t),
+                  onTap: () {
+                    setState(() => _expenseType = t);
+                    Navigator.pop(context);
+                  },
+                ))
+            .toList(),
+      ),
+    );
+  }
+
+  void _showRemarkEditor() {
+    showDialog(
+      context: context,
+      builder: (_) => AlertDialog(
+        title: const Text('备注'),
+        content: TextField(
+          controller: _remarkController,
+          maxLines: 3,
+          decoration: const InputDecoration(
+              hintText: '请输入备注信息…', border: OutlineInputBorder()),
+        ),
+        actions: [
+          TextButton(
+              onPressed: () => Navigator.pop(context),
+              child: const Text('取消')),
+          TextButton(
+            onPressed: () {
+              setState(() {});
+              Navigator.pop(context);
+            },
+            child: const Text('确定'),
+          ),
+        ],
+      ),
+    );
+  }
+
+  void _handleSaveDraft() {
+    if (!_formKey.currentState!.validate()) return;
+    ScaffoldMessenger.of(context).showSnackBar(
+      const SnackBar(content: Text('草稿已保存')),
+    );
+    context.pop();
+  }
+
+  void _handleSubmit() {
+    if (!_formKey.currentState!.validate()) return;
+    ScaffoldMessenger.of(context).showSnackBar(
+      const SnackBar(content: Text('提交成功')),
+    );
+    context.pop();
+  }
+}

+ 121 - 0
lib/features/expense_application/expense_application_detail_page.dart

@@ -0,0 +1,121 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import '../../core/theme/app_colors.dart';
+import '../../core/utils/date_utils.dart' as du;
+import '../../core/utils/responsive.dart';
+import '../../shared/widgets/form_section.dart';
+import '../../shared/widgets/form_field_row.dart';
+import '../../shared/widgets/status_tag.dart';
+import '../../shared/widgets/loading_widget.dart';
+import 'expense_application_model.dart';
+
+final expenseApplicationDetailProvider =
+    FutureProvider.autoDispose.family<ExpenseApplicationModel, String>(
+        (ref, id) async {
+  // mock detail — 返回硬编码数据
+  return ExpenseApplicationModel(
+    id: id,
+    applicationNo: 'BXSQ-20240501-001',
+    applicantId: 'U001',
+    applicantName: '张三',
+    deptId: 'D001',
+    deptName: '销售部',
+    expenseType: '差旅费',
+    estimatedAmount: 3500.00,
+    purpose: '上海出差拜访客户',
+    remark: '',
+    status: 'pending',
+    currentApproverId: 'U100',
+    approvalChain: ['U100', 'U200'],
+    createTime: DateTime(2024, 5, 1, 14, 30),
+    updateTime: DateTime(2024, 5, 1, 14, 30),
+  );
+});
+
+class ExpenseApplicationDetailPage extends ConsumerWidget {
+  final String id;
+  const ExpenseApplicationDetailPage({super.key, required this.id});
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final detailAsync = ref.watch(expenseApplicationDetailProvider(id));
+    final r = ResponsiveHelper.of(context);
+
+    return Scaffold(
+      appBar: AppBar(title: const Text('报销申请详情')),
+      body: detailAsync.when(
+        loading: () => const LoadingWidget(),
+        error: (_, _) => const Center(child: Text('加载失败')),
+        data: (model) => Center(
+          child: ConstrainedBox(
+            constraints:
+                BoxConstraints(maxWidth: r.detailTwoColumns ? 700 : double.infinity),
+            child: SingleChildScrollView(
+              padding: const EdgeInsets.symmetric(vertical: 8),
+              child: Column(
+                children: [
+                  FormSection(
+                    title: '基本信息',
+                    children: [
+                      FormFieldRow(
+                          label: '申请单号',
+                          value: model.applicationNo,
+                          showArrow: false),
+                      FormFieldRow(
+                          label: '费用类型',
+                          value: model.expenseType,
+                          showArrow: false),
+                      FormFieldRow(
+                          label: '预计金额',
+                          value: '¥${model.estimatedAmount.toStringAsFixed(2)}',
+                          showArrow: false),
+                      FormFieldRow(
+                          label: '部门',
+                          value: model.deptName,
+                          showArrow: false),
+                      FormFieldRow(
+                          label: '申请人',
+                          value: model.applicantName,
+                          showArrow: false),
+                      FormFieldRow(
+                          label: '创建时间',
+                          value: du.DateUtils.formatDateTime(model.createTime),
+                          showArrow: false),
+                      FormFieldRow(
+                          label: '状态',
+                          value: '',
+                          showArrow: false,
+                          onTap: null),
+                      Padding(
+                        padding: const EdgeInsets.fromLTRB(14, 0, 14, 12),
+                        child: Row(
+                          children: [
+                            const SizedBox(
+                                width: 72,
+                                child: Text('状态',
+                                    style: TextStyle(
+                                        color: AppColors.textSecondary,
+                                        fontSize: 13))),
+                            StatusTag(status: model.status),
+                          ],
+                        ),
+                      ),
+                      FormFieldRow(
+                          label: '用途说明',
+                          value: model.purpose.isEmpty ? '-' : model.purpose,
+                          showArrow: false),
+                      FormFieldRow(
+                          label: '备注',
+                          value: model.remark.isEmpty ? '-' : model.remark,
+                          showArrow: false),
+                    ],
+                  ),
+                ],
+              ),
+            ),
+          ),
+        ),
+      ),
+    );
+  }
+}

+ 45 - 0
lib/features/expense_application/expense_application_list_controller.dart

@@ -0,0 +1,45 @@
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'expense_application_model.dart';
+
+final expenseApplyStatusFilterProvider = StateProvider<String>((ref) => '');
+
+final expenseApplicationListProvider =
+    FutureProvider.autoDispose<List<ExpenseApplicationModel>>((ref) async {
+  // mock data — 硬编码 2 条
+  return [
+    ExpenseApplicationModel(
+      id: 'EA001',
+      applicationNo: 'BXSQ-20240501-001',
+      applicantId: 'U001',
+      applicantName: '张三',
+      deptId: 'D001',
+      deptName: '销售部',
+      expenseType: '差旅费',
+      estimatedAmount: 3500.00,
+      purpose: '上海出差拜访客户',
+      remark: '',
+      status: 'pending',
+      currentApproverId: 'U100',
+      approvalChain: ['U100', 'U200'],
+      createTime: DateTime(2024, 5, 1, 14, 30),
+      updateTime: DateTime(2024, 5, 1, 14, 30),
+    ),
+    ExpenseApplicationModel(
+      id: 'EA002',
+      applicationNo: 'BXSQ-20240428-002',
+      applicantId: 'U002',
+      applicantName: '李四',
+      deptId: 'D002',
+      deptName: '技术部',
+      expenseType: '办公用品',
+      estimatedAmount: 1560.00,
+      purpose: '采购开发板及配件',
+      remark: '急用',
+      status: 'approved',
+      currentApproverId: '',
+      approvalChain: ['U100', 'U200'],
+      createTime: DateTime(2024, 4, 28, 9, 15),
+      updateTime: DateTime(2024, 4, 29, 10, 0),
+    ),
+  ];
+});

+ 163 - 0
lib/features/expense_application/expense_application_list_page.dart

@@ -0,0 +1,163 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:go_router/go_router.dart';
+import '../../core/theme/app_colors.dart';
+import '../../core/utils/date_utils.dart' as du;
+import '../../core/utils/responsive.dart';
+import '../../shared/widgets/app_card.dart';
+import '../../shared/widgets/status_tag.dart';
+import '../../shared/widgets/empty_state.dart';
+import '../../shared/widgets/loading_widget.dart';
+import 'expense_application_list_controller.dart';
+import 'expense_application_model.dart';
+
+class ExpenseApplicationListPage extends ConsumerStatefulWidget {
+  const ExpenseApplicationListPage({super.key});
+  @override
+  ConsumerState<ExpenseApplicationListPage> createState() =>
+      _ExpenseApplicationListPageState();
+}
+
+class _ExpenseApplicationListPageState
+    extends ConsumerState<ExpenseApplicationListPage> {
+  @override
+  Widget build(BuildContext context) {
+    final status = ref.watch(expenseApplyStatusFilterProvider);
+    final itemsAsync = ref.watch(expenseApplicationListProvider);
+    final r = ResponsiveHelper.of(context);
+
+    return Scaffold(
+      appBar: AppBar(
+        title: const Text('报销申请'),
+        actions: [
+          TextButton(
+            onPressed: () => context.push('/expense-apply/apply'),
+            child: const Text('+ 新建',
+                style: TextStyle(color: Colors.white, fontSize: 13)),
+          ),
+        ],
+      ),
+      body: Column(
+        children: [
+          _buildStatusFilter(status, r),
+          Expanded(
+            child: Center(
+              child: ConstrainedBox(
+                constraints: BoxConstraints(maxWidth: r.listMaxWidth),
+                child: itemsAsync.when(
+                  loading: () => const LoadingWidget(),
+                  error: (_, _) => const EmptyState(message: '加载失败'),
+                  data: (items) => items.isEmpty
+                      ? const EmptyState(message: '暂无报销申请')
+                      : ListView.builder(
+                          padding: const EdgeInsets.symmetric(vertical: 4),
+                          itemCount: items.length,
+                          itemBuilder: (_, index) =>
+                              _buildItem(items[index]),
+                        ),
+                ),
+              ),
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+
+  Widget _buildStatusFilter(String current, ResponsiveHelper r) {
+    final statuses = [
+      {'key': '', 'label': '全部'},
+      {'key': 'pending', 'label': '待审批'},
+      {'key': 'approved', 'label': '已通过'},
+      {'key': 'rejected', 'label': '已拒绝'},
+    ];
+    final filterBar = SingleChildScrollView(
+      scrollDirection: Axis.horizontal,
+      padding: const EdgeInsets.symmetric(horizontal: 12),
+      child: Row(
+        children: statuses.map((s) {
+          final isSelected = current == s['key'];
+          return Padding(
+            padding: const EdgeInsets.only(right: 8),
+            child: GestureDetector(
+              onTap: () {
+                ref.read(expenseApplyStatusFilterProvider.notifier).state =
+                    s['key']!;
+              },
+              child: Container(
+                padding:
+                    const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
+                decoration: BoxDecoration(
+                  color: isSelected ? AppColors.primaryLight : Colors.white,
+                  borderRadius: BorderRadius.circular(16),
+                  border: Border.all(
+                    color: isSelected
+                        ? AppColors.primary
+                        : const Color(0xFFDDDDDD),
+                  ),
+                ),
+                child: Text(
+                  s['label']!,
+                  style: TextStyle(
+                    color: isSelected
+                        ? AppColors.primary
+                        : AppColors.textSecondary,
+                    fontSize: 12,
+                  ),
+                ),
+              ),
+            ),
+          );
+        }).toList(),
+      ),
+    );
+    return Container(
+      color: Colors.white,
+      padding: const EdgeInsets.symmetric(vertical: 8),
+      child: r.isWide
+          ? Center(
+              child: SizedBox(width: r.listMaxWidth, child: filterBar))
+          : filterBar,
+    );
+  }
+
+  Widget _buildItem(ExpenseApplicationModel item) {
+    return AppCard(
+      onTap: () => context.push('/expense-apply/detail/${item.id}'),
+      child: Column(
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: [
+          Row(
+            mainAxisAlignment: MainAxisAlignment.spaceBetween,
+            children: [
+              Text(item.applicationNo,
+                  style: const TextStyle(
+                      fontWeight: FontWeight.w600,
+                      fontSize: 14,
+                      color: AppColors.textPrimary)),
+              StatusTag(status: item.status),
+            ],
+          ),
+          const SizedBox(height: 4),
+          Text(
+            '${item.expenseType} · ¥${item.estimatedAmount.toStringAsFixed(2)}',
+            style: const TextStyle(
+                color: AppColors.textSecondary, fontSize: 12),
+          ),
+          const SizedBox(height: 4),
+          Row(
+            mainAxisAlignment: MainAxisAlignment.spaceBetween,
+            children: [
+              Text(item.applicantName,
+                  style: const TextStyle(
+                      color: AppColors.textHint, fontSize: 11)),
+              Text(du.DateUtils.formatDateTime(item.createTime),
+                  style: const TextStyle(
+                      color: AppColors.textHint, fontSize: 11)),
+            ],
+          ),
+        ],
+      ),
+    );
+  }
+}

+ 174 - 0
lib/features/expense_application/expense_application_model.dart

@@ -0,0 +1,174 @@
+import '../../shared/models/approval_status.dart';
+
+class ExpenseApplicationModel {
+  final String id;
+  final String applicationNo;
+  final String applicantId;
+  final String applicantName;
+  final String deptId;
+  final String deptName;
+  final String expenseType;
+  final double estimatedAmount;
+  final String purpose;
+  final String remark;
+  final String status;
+  final String currentApproverId;
+  final List<String> approvalChain;
+  final DateTime createTime;
+  final DateTime updateTime;
+  final List<ExpenseAppDetailModel> details;
+  final List<ApprovalRecord> approvalRecords;
+
+  const ExpenseApplicationModel({
+    required this.id,
+    required this.applicationNo,
+    this.applicantId = '',
+    this.applicantName = '',
+    this.deptId = '',
+    this.deptName = '',
+    this.expenseType = '',
+    this.estimatedAmount = 0.0,
+    this.purpose = '',
+    this.remark = '',
+    this.status = 'draft',
+    this.currentApproverId = '',
+    this.approvalChain = const [],
+    required this.createTime,
+    required this.updateTime,
+    this.details = const [],
+    this.approvalRecords = const [],
+  });
+
+  factory ExpenseApplicationModel.fromJson(Map<String, dynamic> json) {
+    return ExpenseApplicationModel(
+      id: json['id'] as String,
+      applicationNo: json['applicationNo'] as String? ?? '',
+      applicantId: json['applicantId'] as String? ?? '',
+      applicantName: json['applicantName'] as String? ?? '',
+      deptId: json['deptId'] as String? ?? '',
+      deptName: json['deptName'] as String? ?? '',
+      expenseType: json['expenseType'] as String? ?? '',
+      estimatedAmount: (json['estimatedAmount'] as num?)?.toDouble() ?? 0.0,
+      purpose: json['purpose'] as String? ?? '',
+      remark: json['remark'] as String? ?? '',
+      status: json['status'] as String? ?? 'draft',
+      currentApproverId: json['currentApproverId'] as String? ?? '',
+      approvalChain:
+          (json['approvalChain'] as List<dynamic>?)
+              ?.map((e) => e as String)
+              .toList() ??
+          [],
+      createTime: DateTime.parse(json['createTime'] as String),
+      updateTime: DateTime.parse(json['updateTime'] as String),
+      details:
+          (json['details'] as List<dynamic>?)
+              ?.map((e) =>
+                  ExpenseAppDetailModel.fromJson(e as Map<String, dynamic>))
+              .toList() ??
+          [],
+      approvalRecords:
+          (json['approvalRecords'] as List<dynamic>?)
+              ?.map((e) => ApprovalRecord.fromJson(e as Map<String, dynamic>))
+              .toList() ??
+          [],
+    );
+  }
+
+  Map<String, dynamic> toJson() => {
+        'id': id,
+        'applicationNo': applicationNo,
+        'applicantId': applicantId,
+        'applicantName': applicantName,
+        'deptId': deptId,
+        'deptName': deptName,
+        'expenseType': expenseType,
+        'estimatedAmount': estimatedAmount,
+        'purpose': purpose,
+        'remark': remark,
+        'status': status,
+        'currentApproverId': currentApproverId,
+        'approvalChain': approvalChain,
+        'createTime': createTime.toIso8601String(),
+        'updateTime': updateTime.toIso8601String(),
+        'details': details.map((d) => d.toJson()).toList(),
+        'approvalRecords': approvalRecords.map((r) => r.toJson()).toList(),
+      };
+
+  ExpenseApplicationModel copyWith({
+    String? id,
+    String? applicationNo,
+    String? applicantId,
+    String? applicantName,
+    String? deptId,
+    String? deptName,
+    String? expenseType,
+    double? estimatedAmount,
+    String? purpose,
+    String? remark,
+    String? status,
+    String? currentApproverId,
+    List<String>? approvalChain,
+    DateTime? createTime,
+    DateTime? updateTime,
+    List<ExpenseAppDetailModel>? details,
+    List<ApprovalRecord>? approvalRecords,
+  }) {
+    return ExpenseApplicationModel(
+      id: id ?? this.id,
+      applicationNo: applicationNo ?? this.applicationNo,
+      applicantId: applicantId ?? this.applicantId,
+      applicantName: applicantName ?? this.applicantName,
+      deptId: deptId ?? this.deptId,
+      deptName: deptName ?? this.deptName,
+      expenseType: expenseType ?? this.expenseType,
+      estimatedAmount: estimatedAmount ?? this.estimatedAmount,
+      purpose: purpose ?? this.purpose,
+      remark: remark ?? this.remark,
+      status: status ?? this.status,
+      currentApproverId: currentApproverId ?? this.currentApproverId,
+      approvalChain: approvalChain ?? this.approvalChain,
+      createTime: createTime ?? this.createTime,
+      updateTime: updateTime ?? this.updateTime,
+      details: details ?? this.details,
+      approvalRecords: approvalRecords ?? this.approvalRecords,
+    );
+  }
+}
+
+class ExpenseAppDetailModel {
+  final String id;
+  final String applicationId;
+  final String itemName;
+  final double estimatedAmount;
+  final String remark;
+  final int sortOrder;
+
+  const ExpenseAppDetailModel({
+    required this.id,
+    this.applicationId = '',
+    this.itemName = '',
+    this.estimatedAmount = 0.0,
+    this.remark = '',
+    this.sortOrder = 1,
+  });
+
+  factory ExpenseAppDetailModel.fromJson(Map<String, dynamic> json) {
+    return ExpenseAppDetailModel(
+      id: json['id'] as String,
+      applicationId: json['applicationId'] as String? ?? '',
+      itemName: json['itemName'] as String? ?? '',
+      estimatedAmount: (json['estimatedAmount'] as num?)?.toDouble() ?? 0.0,
+      remark: json['remark'] as String? ?? '',
+      sortOrder: json['sortOrder'] as int? ?? 1,
+    );
+  }
+
+  Map<String, dynamic> toJson() => {
+        'id': id,
+        'applicationId': applicationId,
+        'itemName': itemName,
+        'estimatedAmount': estimatedAmount,
+        'remark': remark,
+        'sortOrder': sortOrder,
+      };
+}

+ 13 - 18
lib/features/home/home_controller.dart

@@ -1,10 +1,10 @@
 import 'package:flutter_riverpod/flutter_riverpod.dart';
-import '../../app.dart';
 
 class HomeSummary {
   final int expensePending;
   final int overtimePending;
   final int vehiclePending;
+  final int expenseApplyPending;
   final int logCount;
   final int announcementCount;
   final int announcementUnread;
@@ -14,29 +14,24 @@ class HomeSummary {
     required this.expensePending,
     required this.overtimePending,
     required this.vehiclePending,
+    this.expenseApplyPending = 0,
     required this.logCount,
     required this.announcementCount,
     this.announcementUnread = 0,
     required this.totalCount,
   });
-
-  factory HomeSummary.fromJson(dynamic json) {
-    final map = json as Map<String, dynamic>;
-    return HomeSummary(
-      expensePending: map['expensePending'] as int? ?? 0,
-      overtimePending: map['overtimePending'] as int? ?? 0,
-      vehiclePending: map['vehiclePending'] as int? ?? 0,
-      logCount: map['logCount'] as int? ?? 0,
-      announcementCount: map['announcementCount'] as int? ?? 0,
-      announcementUnread: map['announcementUnread'] as int? ?? 0,
-      totalCount: map['totalCount'] as int? ?? 0,
-    );
-  }
 }
 
 final homeSummaryProvider = FutureProvider<HomeSummary>((ref) async {
-  final client = ref.read(apiClientProvider);
-  final response =
-      await client.get('/home/summary', fromJsonT: HomeSummary.fromJson);
-  return response.data!;
+  await Future.delayed(const Duration(milliseconds: 300));
+  return const HomeSummary(
+    expensePending: 3,
+    overtimePending: 1,
+    vehiclePending: 2,
+    expenseApplyPending: 1,
+    logCount: 12,
+    announcementCount: 5,
+    announcementUnread: 2,
+    totalCount: 28,
+  );
 });

+ 45 - 1
lib/features/home/home_page.dart

@@ -55,7 +55,7 @@ class HomePage extends ConsumerWidget {
             items: [
               _EntryItem(
                 icon: Icons.receipt_long,
-                label: '报销',
+                label: '报销',
                 badge: '${summary.expensePending}待办',
                 color: AppColors.primary,
                 onTap: () => context.push('/expense/list'),
@@ -74,6 +74,15 @@ class HomePage extends ConsumerWidget {
                 color: AppColors.warning,
                 onTap: () => context.push('/vehicle/list'),
               ),
+              _EntryItem(
+                icon: Icons.assignment,
+                label: '报销申请',
+                badge: summary.expenseApplyPending > 0
+                    ? '${summary.expenseApplyPending}待办'
+                    : '0待办',
+                color: Colors.deepPurple,
+                onTap: () => context.push('/expense-apply/list'),
+              ),
             ],
           ),
           const SizedBox(height: 12),
@@ -106,6 +115,41 @@ class HomePage extends ConsumerWidget {
               ),
             ],
           ),
+          const SizedBox(height: 12),
+          _buildSection(
+            title: '报表中心',
+            r: r,
+            items: [
+              _EntryItem(
+                icon: Icons.pie_chart,
+                label: '报销明细表',
+                badge: '',
+                color: AppColors.primary,
+                onTap: () => context.push('/report/expense-detail'),
+              ),
+              _EntryItem(
+                icon: Icons.bar_chart,
+                label: '加班明细表',
+                badge: '',
+                color: AppColors.success,
+                onTap: () => context.push('/report/overtime-detail'),
+              ),
+              _EntryItem(
+                icon: Icons.directions_car,
+                label: '用车明细表',
+                badge: '',
+                color: AppColors.warning,
+                onTap: () => context.push('/report/vehicle-detail'),
+              ),
+              _EntryItem(
+                icon: Icons.receipt,
+                label: '报销申请明细表',
+                badge: '',
+                color: Colors.deepPurple,
+                onTap: () => context.push('/report/expense-apply-detail'),
+              ),
+            ],
+          ),
         ],
       ),
     );

+ 21 - 2
lib/features/outing_log/outing_log_detail_page.dart

@@ -5,11 +5,30 @@ import '../../core/utils/date_utils.dart' as du;
 import '../../core/utils/responsive.dart';
 import '../../shared/widgets/form_section.dart';
 import '../../shared/widgets/form_field_row.dart';
-import 'outing_log_api.dart';
 import 'outing_log_model.dart';
 
 final outingLogDetailProvider = FutureProvider.autoDispose.family<OutingLogModel, String>((ref, id) async {
-  return ref.read(outingLogApiProvider).fetchDetail(id);
+  await Future.delayed(const Duration(milliseconds: 300));
+  return OutingLogModel(
+    id: id,
+    visitNo: 'VL202605001',
+    salespersonId: 'u-001',
+    salespersonName: '张三',
+    deptId: 'dept-001',
+    deptName: '市场部',
+    customerName: '华软科技',
+    visitDate: DateTime(2026, 5, 20),
+    visitStartTime: DateTime(2026, 5, 20, 9, 0),
+    visitEndTime: DateTime(2026, 5, 20, 11, 30),
+    visitType: '常规拜访',
+    visitPurpose: '产品演示及方案交流',
+    visitLocation: '深圳市南山区科技园',
+    visitSummary: '向客户展示了公司最新产品功能,客户对数据看板功能比较感兴趣,约定下周安排试用。',
+    nextVisitTime: DateTime(2026, 5, 27),
+    status: '已完成',
+    createTime: DateTime(2026, 5, 20),
+    updateTime: DateTime(2026, 5, 20),
+  );
 });
 
 class OutingLogDetailPage extends ConsumerWidget {

+ 51 - 4
lib/features/outing_log/outing_log_list_controller.dart

@@ -1,11 +1,58 @@
 import 'package:flutter_riverpod/flutter_riverpod.dart';
 import 'outing_log_model.dart';
-import 'outing_log_api.dart';
+
+final _mockLogs = <OutingLogModel>[
+  OutingLogModel(
+    id: 'log-001',
+    visitNo: 'VL202605001',
+    salespersonId: 'u-001',
+    salespersonName: '张三',
+    deptId: 'dept-001',
+    deptName: '市场部',
+    customerName: '华软科技',
+    visitDate: DateTime(2026, 5, 20),
+    visitStartTime: DateTime(2026, 5, 20, 9, 0),
+    visitEndTime: DateTime(2026, 5, 20, 11, 30),
+    visitType: '常规拜访',
+    visitPurpose: '产品演示及方案交流',
+    visitLocation: '深圳市南山区科技园',
+    visitSummary: '向客户展示了公司最新产品功能,客户对数据看板功能比较感兴趣,约定下周安排试用。',
+    nextVisitTime: DateTime(2026, 5, 27),
+    status: '已完成',
+    createTime: DateTime(2026, 5, 20),
+    updateTime: DateTime(2026, 5, 20),
+  ),
+  OutingLogModel(
+    id: 'log-002',
+    visitNo: 'VL202605002',
+    salespersonId: 'u-002',
+    salespersonName: '王五',
+    deptId: 'dept-002',
+    deptName: '技术部',
+    customerName: '云创数据',
+    customerId: 'cust-002',
+    contactName: '陈经理',
+    contactPhone: '13800138002',
+    contactPosition: '技术总监',
+    visitDate: DateTime(2026, 5, 22),
+    visitStartTime: DateTime(2026, 5, 22, 14, 0),
+    visitEndTime: DateTime(2026, 5, 22, 16, 0),
+    visitType: '技术交流',
+    visitPurpose: '技术对接方案确认',
+    visitLocation: '广州市天河区软件路',
+    visitSummary: '与客户技术团队确认了API对接方案,明确了数据字段映射关系,预计下周开始联调。',
+    visitResult: '达成合作意向',
+    checkInStatus: '正常',
+    nextVisitTime: DateTime(2026, 5, 29),
+    status: '已完成',
+    createTime: DateTime(2026, 5, 22),
+    updateTime: DateTime(2026, 5, 22),
+  ),
+];
 
 final outingLogListProvider =
     FutureProvider.autoDispose.family<List<OutingLogModel>, int>(
         (ref, page) async {
-  final api = ref.watch(outingLogApiProvider);
-  final result = await api.fetchList(page: page);
-  return result.list;
+  await Future.delayed(const Duration(milliseconds: 300));
+  return _mockLogs;
 });

+ 33 - 2
lib/features/overtime/overtime_detail_page.dart

@@ -3,14 +3,45 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
 import '../../core/theme/app_colors.dart';
 import '../../core/utils/date_utils.dart' as du;
 import '../../core/utils/responsive.dart';
+import '../../shared/models/approval_status.dart';
 import '../../shared/widgets/form_section.dart';
 import '../../shared/widgets/form_field_row.dart';
 import '../../shared/widgets/status_tag.dart';
-import 'overtime_api.dart';
 import 'overtime_model.dart';
 
 final overtimeDetailProvider = FutureProvider.autoDispose.family<OvertimeModel, String>((ref, id) async {
-  return ref.read(overtimeApiProvider).fetchDetail(id);
+  await Future.delayed(const Duration(milliseconds: 300));
+  return OvertimeModel(
+    id: id,
+    applicationNo: 'OT202605001',
+    applicantId: 'u-001',
+    applicantName: '张三',
+    deptId: 'dept-001',
+    deptName: '市场部',
+    otDate: DateTime(2026, 5, 20),
+    startTime: DateTime(2026, 5, 20, 18, 0),
+    endTime: DateTime(2026, 5, 20, 21, 0),
+    otHours: 3.0,
+    otType: '工作日加班',
+    compensationType: '加班费',
+    reason: '项目上线前紧急测试',
+    status: 'pending',
+    createTime: DateTime(2026, 5, 20),
+    updateTime: DateTime(2026, 5, 20),
+    approvalRecords: [
+      ApprovalRecord(
+        id: 'ar-ot-001',
+        bizId: id,
+        bizType: 'overtime',
+        approverId: 'u-mgr',
+        approverName: '李四',
+        approvalLevel: 1,
+        action: 'pending',
+        opinion: '',
+        approvalTime: DateTime(2026, 5, 20),
+      ),
+    ],
+  );
 });
 
 class OvertimeDetailPage extends ConsumerWidget {

+ 102 - 4
lib/features/overtime/overtime_list_controller.dart

@@ -1,14 +1,112 @@
 import 'package:flutter_riverpod/flutter_riverpod.dart';
+import '../../shared/models/approval_status.dart';
 import 'overtime_model.dart';
-import 'overtime_api.dart';
 
 final overtimeStatusFilterProvider = StateProvider<String>((ref) => '');
 
+final _mockOvertimes = <OvertimeModel>[
+  OvertimeModel(
+    id: 'ot-001',
+    applicationNo: 'OT202605001',
+    applicantId: 'u-001',
+    applicantName: '张三',
+    deptId: 'dept-001',
+    deptName: '市场部',
+    otDate: DateTime(2026, 5, 20),
+    startTime: DateTime(2026, 5, 20, 18, 0),
+    endTime: DateTime(2026, 5, 20, 21, 0),
+    otHours: 3.0,
+    otType: '工作日加班',
+    compensationType: '加班费',
+    reason: '项目上线前紧急测试',
+    status: 'pending',
+    createTime: DateTime(2026, 5, 20),
+    updateTime: DateTime(2026, 5, 20),
+    approvalRecords: [
+      ApprovalRecord(
+        id: 'ar-ot-001',
+        bizId: 'ot-001',
+        bizType: 'overtime',
+        approverId: 'u-mgr',
+        approverName: '李四',
+        approvalLevel: 1,
+        action: 'pending',
+        opinion: '',
+        approvalTime: DateTime(2026, 5, 20),
+      ),
+    ],
+  ),
+  OvertimeModel(
+    id: 'ot-002',
+    applicationNo: 'OT202605002',
+    applicantId: 'u-002',
+    applicantName: '王五',
+    deptId: 'dept-002',
+    deptName: '技术部',
+    otDate: DateTime(2026, 5, 18),
+    startTime: DateTime(2026, 5, 18, 9, 0),
+    endTime: DateTime(2026, 5, 18, 18, 0),
+    otHours: 8.0,
+    otType: '休息日加班',
+    compensationType: '调休',
+    reason: '系统架构升级',
+    status: 'approved',
+    createTime: DateTime(2026, 5, 17),
+    updateTime: DateTime(2026, 5, 19),
+    approvalRecords: [
+      ApprovalRecord(
+        id: 'ar-ot-002',
+        bizId: 'ot-002',
+        bizType: 'overtime',
+        approverId: 'u-mgr',
+        approverName: '李四',
+        approvalLevel: 1,
+        action: 'approve',
+        opinion: '同意',
+        approvalTime: DateTime(2026, 5, 19),
+      ),
+    ],
+  ),
+  OvertimeModel(
+    id: 'ot-003',
+    applicationNo: 'OT202605003',
+    applicantId: 'u-003',
+    applicantName: '赵六',
+    deptId: 'dept-003',
+    deptName: '财务部',
+    otDate: DateTime(2026, 5, 25),
+    startTime: DateTime(2026, 5, 25, 19, 0),
+    endTime: DateTime(2026, 5, 25, 22, 0),
+    otHours: 3.0,
+    otType: '工作日加班',
+    compensationType: '加班费',
+    reason: '季度财务报表汇总',
+    status: 'pending',
+    createTime: DateTime(2026, 5, 25),
+    updateTime: DateTime(2026, 5, 25),
+    approvalRecords: [
+      ApprovalRecord(
+        id: 'ar-ot-003',
+        bizId: 'ot-003',
+        bizType: 'overtime',
+        approverId: 'u-mgr',
+        approverName: '李四',
+        approvalLevel: 1,
+        action: 'pending',
+        opinion: '',
+        approvalTime: DateTime(2026, 5, 25),
+      ),
+    ],
+  ),
+];
+
 final overtimeListProvider =
     FutureProvider.autoDispose.family<List<OvertimeModel>, int>(
         (ref, page) async {
+  await Future.delayed(const Duration(milliseconds: 300));
   final status = ref.watch(overtimeStatusFilterProvider);
-  final api = ref.watch(overtimeApiProvider);
-  final result = await api.fetchList(status: status, page: page);
-  return result.list;
+  if (status.isEmpty) {
+    return _mockOvertimes;
+  }
+  return _mockOvertimes.where((e) => e.status == status).toList();
 });

+ 224 - 0
lib/features/report/expense_apply_detail_report_page.dart

@@ -0,0 +1,224 @@
+import 'package:flutter/material.dart';
+import 'package:fl_chart/fl_chart.dart';
+import '../../core/theme/app_colors.dart';
+
+class ExpenseApplyDetailReportPage extends StatelessWidget {
+  const ExpenseApplyDetailReportPage({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(title: const Text('报销申请明细表')),
+      body: SingleChildScrollView(
+        padding: const EdgeInsets.all(12),
+        child: Column(
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: [
+            _buildSummaryRow(),
+            const SizedBox(height: 16),
+            const Text('月度趋势',
+                style: TextStyle(
+                    fontSize: 15,
+                    fontWeight: FontWeight.w600,
+                    color: AppColors.textPrimary)),
+            const SizedBox(height: 12),
+            SizedBox(
+              height: 220,
+              child: _buildBarChart(),
+            ),
+            const SizedBox(height: 16),
+            const Text('明细列表',
+                style: TextStyle(
+                    fontSize: 15,
+                    fontWeight: FontWeight.w600,
+                    color: AppColors.textPrimary)),
+            const SizedBox(height: 8),
+            _buildDetailList(),
+          ],
+        ),
+      ),
+    );
+  }
+
+  Widget _buildSummaryRow() {
+    return Row(
+      children: [
+        Expanded(
+          child: _summaryCard('申请总金额', '¥15,800.00', AppColors.primary),
+        ),
+        const SizedBox(width: 8),
+        Expanded(
+          child: _summaryCard('申请笔数', '18 笔', AppColors.success),
+        ),
+        const SizedBox(width: 8),
+        Expanded(
+          child: _summaryCard('待审批', '5 笔', AppColors.warning),
+        ),
+      ],
+    );
+  }
+
+  Widget _summaryCard(String label, String value, Color color) {
+    return Container(
+      padding: const EdgeInsets.all(12),
+      decoration: BoxDecoration(
+        color: color.withValues(alpha: 0.08),
+        borderRadius: BorderRadius.circular(10),
+      ),
+      child: Column(
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: [
+          Text(label,
+              style: const TextStyle(
+                  fontSize: 11, color: AppColors.textSecondary)),
+          const SizedBox(height: 4),
+          Text(value,
+              style: TextStyle(
+                  fontSize: 16,
+                  fontWeight: FontWeight.bold,
+                  color: color)),
+        ],
+      ),
+    );
+  }
+
+  Widget _buildBarChart() {
+    final data = [
+      ('1月', 3200.0),
+      ('2月', 1800.0),
+      ('3月', 4500.0),
+      ('4月', 3800.0),
+      ('5月', 2500.0),
+      ('6月', 0.0),
+    ];
+
+    return BarChart(
+      BarChartData(
+        alignment: BarChartAlignment.spaceAround,
+        maxY: 5000,
+        barGroups: data.asMap().entries.map((entry) {
+          final i = entry.key;
+          final (_, value) = entry.value;
+          return BarChartGroupData(
+            x: i,
+            barRods: [
+              BarChartRodData(
+                toY: value,
+                color: AppColors.success,
+                width: 20,
+                borderRadius: const BorderRadius.only(
+                    topLeft: Radius.circular(4),
+                    topRight: Radius.circular(4)),
+              ),
+            ],
+          );
+        }).toList(),
+        titlesData: FlTitlesData(
+          leftTitles: AxisTitles(
+            sideTitles: SideTitles(
+              showTitles: true,
+              reservedSize: 36,
+              getTitlesWidget: (value, meta) => Text(
+                '${(value / 1000).toInt()}k',
+                style: const TextStyle(
+                    fontSize: 10, color: AppColors.textHint),
+              ),
+            ),
+          ),
+          bottomTitles: AxisTitles(
+            sideTitles: SideTitles(
+              showTitles: true,
+              getTitlesWidget: (value, meta) {
+                final idx = value.toInt();
+                if (idx < 0 || idx >= data.length) return const SizedBox();
+                return Padding(
+                  padding: const EdgeInsets.only(top: 4),
+                  child: Text(data[idx].$1,
+                      style: const TextStyle(
+                          fontSize: 10, color: AppColors.textHint)),
+                );
+              },
+            ),
+          ),
+          topTitles: const AxisTitles(
+              sideTitles: SideTitles(showTitles: false)),
+          rightTitles: const AxisTitles(
+              sideTitles: SideTitles(showTitles: false)),
+        ),
+        gridData: FlGridData(
+          show: true,
+          drawVerticalLine: false,
+          horizontalInterval: 1000,
+          getDrawingHorizontalLine: (value) => FlLine(
+            color: AppColors.divider,
+            strokeWidth: 0.5,
+          ),
+        ),
+        borderData: FlBorderData(show: false),
+      ),
+    );
+  }
+
+  Widget _buildDetailList() {
+    final items = [
+      {'no': 'BXSQ-20240501-001', 'applicant': '张三', 'amount': '¥3,500.00', 'status': '待审批'},
+      {'no': 'BXSQ-20240428-002', 'applicant': '李四', 'amount': '¥1,560.00', 'status': '已通过'},
+      {'no': 'BXSQ-20240425-003', 'applicant': '王五', 'amount': '¥2,800.00', 'status': '已拒绝'},
+      {'no': 'BXSQ-20240420-004', 'applicant': '赵六', 'amount': '¥890.00', 'status': '已通过'},
+    ];
+    return Column(
+      children: items
+          .map((item) => Container(
+                margin: const EdgeInsets.only(bottom: 6),
+                padding: const EdgeInsets.all(12),
+                decoration: BoxDecoration(
+                  color: Colors.white,
+                  borderRadius: BorderRadius.circular(8),
+                  boxShadow: [
+                    BoxShadow(
+                        color: Colors.black.withValues(alpha: 0.04),
+                        blurRadius: 4,
+                        offset: const Offset(0, 1)),
+                  ],
+                ),
+                child: Row(
+                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                  children: [
+                    Column(
+                      crossAxisAlignment: CrossAxisAlignment.start,
+                      children: [
+                        Text(item['no']!,
+                            style: const TextStyle(
+                                fontSize: 13,
+                                color: AppColors.textPrimary)),
+                        Text(item['applicant']!,
+                            style: const TextStyle(
+                                fontSize: 10,
+                                color: AppColors.textHint)),
+                      ],
+                    ),
+                    Column(
+                      crossAxisAlignment: CrossAxisAlignment.end,
+                      children: [
+                        Text(item['amount']!,
+                            style: const TextStyle(
+                                fontSize: 14,
+                                fontWeight: FontWeight.w600,
+                                color: AppColors.primary)),
+                        Text(item['status']!,
+                            style: TextStyle(
+                                fontSize: 10,
+                                color: item['status'] == '待审批'
+                                    ? AppColors.warning
+                                    : item['status'] == '已通过'
+                                        ? AppColors.success
+                                        : AppColors.error)),
+                      ],
+                    ),
+                  ],
+                ),
+              ))
+          .toList(),
+    );
+  }
+}

+ 197 - 0
lib/features/report/expense_detail_report_page.dart

@@ -0,0 +1,197 @@
+import 'package:flutter/material.dart';
+import 'package:fl_chart/fl_chart.dart';
+import '../../core/theme/app_colors.dart';
+
+class ExpenseDetailReportPage extends StatelessWidget {
+  const ExpenseDetailReportPage({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(title: const Text('报销明细表')),
+      body: SingleChildScrollView(
+        padding: const EdgeInsets.all(12),
+        child: Column(
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: [
+            _buildSummaryRow(),
+            const SizedBox(height: 16),
+            const Text('费用类型分布',
+                style: TextStyle(
+                    fontSize: 15,
+                    fontWeight: FontWeight.w600,
+                    color: AppColors.textPrimary)),
+            const SizedBox(height: 12),
+            SizedBox(
+              height: 220,
+              child: _buildPieChart(),
+            ),
+            const SizedBox(height: 16),
+            const Text('明细列表',
+                style: TextStyle(
+                    fontSize: 15,
+                    fontWeight: FontWeight.w600,
+                    color: AppColors.textPrimary)),
+            const SizedBox(height: 8),
+            _buildDetailList(),
+          ],
+        ),
+      ),
+    );
+  }
+
+  Widget _buildSummaryRow() {
+    return Row(
+      children: [
+        Expanded(
+          child: _summaryCard('总报销金额', '¥12,580.00', AppColors.primary),
+        ),
+        const SizedBox(width: 8),
+        Expanded(
+          child: _summaryCard('报销笔数', '23 笔', AppColors.success),
+        ),
+        const SizedBox(width: 8),
+        Expanded(
+          child: _summaryCard('涉及部门', '5 个', AppColors.warning),
+        ),
+      ],
+    );
+  }
+
+  Widget _summaryCard(String label, String value, Color color) {
+    return Container(
+      padding: const EdgeInsets.all(12),
+      decoration: BoxDecoration(
+        color: color.withValues(alpha: 0.08),
+        borderRadius: BorderRadius.circular(10),
+      ),
+      child: Column(
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: [
+          Text(label,
+              style: const TextStyle(
+                  fontSize: 11, color: AppColors.textSecondary)),
+          const SizedBox(height: 4),
+          Text(value,
+              style: TextStyle(
+                  fontSize: 16,
+                  fontWeight: FontWeight.bold,
+                  color: color)),
+        ],
+      ),
+    );
+  }
+
+  Widget _buildPieChart() {
+    const data = [
+      ('差旅费', 5200.0, AppColors.primary),
+      ('办公用品', 2380.0, AppColors.success),
+      ('招待费', 1800.0, AppColors.warning),
+      ('交通费', 1600.0, Color(0xFF722ED1)),
+      ('通讯费', 800.0, Color(0xFFEB2F96)),
+      ('其他', 800.0, AppColors.textHint),
+    ];
+
+    return Row(
+      children: [
+        Expanded(
+          child: PieChart(
+            PieChartData(
+              sections: data.map((d) {
+                final (label, amount, color) = d;
+                return PieChartSectionData(
+                  value: amount,
+                  color: color,
+                  radius: 40,
+                  title: '${(amount / 12580 * 100).toStringAsFixed(0)}%',
+                  titleStyle: const TextStyle(
+                      fontSize: 10,
+                      fontWeight: FontWeight.bold,
+                      color: Colors.white),
+                );
+              }).toList(),
+              centerSpaceRadius: 30,
+              sectionsSpace: 2,
+            ),
+          ),
+        ),
+        const SizedBox(width: 12),
+        Column(
+          mainAxisAlignment: MainAxisAlignment.center,
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: data.map((d) {
+            final (label, amount, color) = d;
+            return Padding(
+              padding: const EdgeInsets.symmetric(vertical: 2),
+              child: Row(
+                mainAxisSize: MainAxisSize.min,
+                children: [
+                  Container(
+                      width: 10,
+                      height: 10,
+                      decoration: BoxDecoration(
+                          color: color,
+                          borderRadius: BorderRadius.circular(2))),
+                  const SizedBox(width: 6),
+                  Text('$label ¥${amount.toStringAsFixed(0)}',
+                      style: const TextStyle(
+                          fontSize: 11, color: AppColors.textSecondary)),
+                ],
+              ),
+            );
+          }).toList(),
+        ),
+      ],
+    );
+  }
+
+  Widget _buildDetailList() {
+    final items = [
+      {'date': '2024-05-01', 'type': '差旅费', 'dept': '销售部', 'amount': '¥2,380.00'},
+      {'date': '2024-04-28', 'type': '办公用品', 'dept': '技术部', 'amount': '¥1,560.00'},
+      {'date': '2024-04-25', 'type': '招待费', 'dept': '销售部', 'amount': '¥890.00'},
+      {'date': '2024-04-20', 'type': '交通费', 'dept': '市场部', 'amount': '¥450.00'},
+    ];
+    return Column(
+      children: items
+          .map((item) => Container(
+                margin: const EdgeInsets.only(bottom: 6),
+                padding: const EdgeInsets.all(12),
+                decoration: BoxDecoration(
+                  color: Colors.white,
+                  borderRadius: BorderRadius.circular(8),
+                  boxShadow: [
+                    BoxShadow(
+                        color: Colors.black.withValues(alpha: 0.04),
+                        blurRadius: 4,
+                        offset: const Offset(0, 1)),
+                  ],
+                ),
+                child: Row(
+                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                  children: [
+                    Column(
+                      crossAxisAlignment: CrossAxisAlignment.start,
+                      children: [
+                        Text(item['type']!,
+                            style: const TextStyle(
+                                fontSize: 13,
+                                color: AppColors.textPrimary)),
+                        Text('${item['date']} · ${item['dept']}',
+                            style: const TextStyle(
+                                fontSize: 10,
+                                color: AppColors.textHint)),
+                      ],
+                    ),
+                    Text(item['amount']!,
+                        style: const TextStyle(
+                            fontSize: 14,
+                            fontWeight: FontWeight.w600,
+                            color: AppColors.primary)),
+                  ],
+                ),
+              ))
+          .toList(),
+    );
+  }
+}

+ 211 - 0
lib/features/report/overtime_detail_report_page.dart

@@ -0,0 +1,211 @@
+import 'package:flutter/material.dart';
+import 'package:fl_chart/fl_chart.dart';
+import '../../core/theme/app_colors.dart';
+
+class OvertimeDetailReportPage extends StatelessWidget {
+  const OvertimeDetailReportPage({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(title: const Text('加班明细表')),
+      body: SingleChildScrollView(
+        padding: const EdgeInsets.all(12),
+        child: Column(
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: [
+            _buildSummaryRow(),
+            const SizedBox(height: 16),
+            const Text('月度趋势',
+                style: TextStyle(
+                    fontSize: 15,
+                    fontWeight: FontWeight.w600,
+                    color: AppColors.textPrimary)),
+            const SizedBox(height: 12),
+            SizedBox(
+              height: 220,
+              child: _buildBarChart(),
+            ),
+            const SizedBox(height: 16),
+            const Text('明细列表',
+                style: TextStyle(
+                    fontSize: 15,
+                    fontWeight: FontWeight.w600,
+                    color: AppColors.textPrimary)),
+            const SizedBox(height: 8),
+            _buildDetailList(),
+          ],
+        ),
+      ),
+    );
+  }
+
+  Widget _buildSummaryRow() {
+    return Row(
+      children: [
+        Expanded(
+          child: _summaryCard('总加班时长', '86.5 h', AppColors.primary),
+        ),
+        const SizedBox(width: 8),
+        Expanded(
+          child: _summaryCard('加班次数', '12 次', AppColors.success),
+        ),
+        const SizedBox(width: 8),
+        Expanded(
+          child: _summaryCard('涉及人数', '8 人', AppColors.warning),
+        ),
+      ],
+    );
+  }
+
+  Widget _summaryCard(String label, String value, Color color) {
+    return Container(
+      padding: const EdgeInsets.all(12),
+      decoration: BoxDecoration(
+        color: color.withValues(alpha: 0.08),
+        borderRadius: BorderRadius.circular(10),
+      ),
+      child: Column(
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: [
+          Text(label,
+              style: const TextStyle(
+                  fontSize: 11, color: AppColors.textSecondary)),
+          const SizedBox(height: 4),
+          Text(value,
+              style: TextStyle(
+                  fontSize: 16,
+                  fontWeight: FontWeight.bold,
+                  color: color)),
+        ],
+      ),
+    );
+  }
+
+  Widget _buildBarChart() {
+    final data = [
+      ('1月', 12.0),
+      ('2月', 8.0),
+      ('3月', 15.0),
+      ('4月', 20.0),
+      ('5月', 18.0),
+      ('6月', 13.5),
+    ];
+
+    return BarChart(
+      BarChartData(
+        alignment: BarChartAlignment.spaceAround,
+        maxY: 25,
+        barGroups: data.asMap().entries.map((entry) {
+          final i = entry.key;
+          final (_, value) = entry.value;
+          return BarChartGroupData(
+            x: i,
+            barRods: [
+              BarChartRodData(
+                toY: value,
+                color: AppColors.primary,
+                width: 20,
+                borderRadius: const BorderRadius.only(
+                    topLeft: Radius.circular(4),
+                    topRight: Radius.circular(4)),
+              ),
+            ],
+          );
+        }).toList(),
+        titlesData: FlTitlesData(
+          leftTitles: AxisTitles(
+            sideTitles: SideTitles(
+              showTitles: true,
+              reservedSize: 30,
+              getTitlesWidget: (value, meta) => Text(
+                '${value.toInt()}',
+                style: const TextStyle(
+                    fontSize: 10, color: AppColors.textHint),
+              ),
+            ),
+          ),
+          bottomTitles: AxisTitles(
+            sideTitles: SideTitles(
+              showTitles: true,
+              getTitlesWidget: (value, meta) {
+                final idx = value.toInt();
+                if (idx < 0 || idx >= data.length) return const SizedBox();
+                return Padding(
+                  padding: const EdgeInsets.only(top: 4),
+                  child: Text(data[idx].$1,
+                      style: const TextStyle(
+                          fontSize: 10, color: AppColors.textHint)),
+                );
+              },
+            ),
+          ),
+          topTitles: const AxisTitles(
+              sideTitles: SideTitles(showTitles: false)),
+          rightTitles: const AxisTitles(
+              sideTitles: SideTitles(showTitles: false)),
+        ),
+        gridData: FlGridData(
+          show: true,
+          drawVerticalLine: false,
+          horizontalInterval: 5,
+          getDrawingHorizontalLine: (value) => FlLine(
+            color: AppColors.divider,
+            strokeWidth: 0.5,
+          ),
+        ),
+        borderData: FlBorderData(show: false),
+      ),
+    );
+  }
+
+  Widget _buildDetailList() {
+    final items = [
+      {'date': '2024-05-03', 'name': '张三', 'hours': '4.0h', 'type': '工作日加班'},
+      {'date': '2024-04-28', 'name': '王五', 'hours': '8.0h', 'type': '休息日加班'},
+      {'date': '2024-04-25', 'name': '李四', 'hours': '3.5h', 'type': '工作日加班'},
+      {'date': '2024-04-20', 'name': '赵六', 'hours': '6.0h', 'type': '节假日加班'},
+    ];
+    return Column(
+      children: items
+          .map((item) => Container(
+                margin: const EdgeInsets.only(bottom: 6),
+                padding: const EdgeInsets.all(12),
+                decoration: BoxDecoration(
+                  color: Colors.white,
+                  borderRadius: BorderRadius.circular(8),
+                  boxShadow: [
+                    BoxShadow(
+                        color: Colors.black.withValues(alpha: 0.04),
+                        blurRadius: 4,
+                        offset: const Offset(0, 1)),
+                  ],
+                ),
+                child: Row(
+                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                  children: [
+                    Column(
+                      crossAxisAlignment: CrossAxisAlignment.start,
+                      children: [
+                        Text(item['name']!,
+                            style: const TextStyle(
+                                fontSize: 13,
+                                color: AppColors.textPrimary)),
+                        Text('${item['date']} · ${item['type']}',
+                            style: const TextStyle(
+                                fontSize: 10,
+                                color: AppColors.textHint)),
+                      ],
+                    ),
+                    Text(item['hours']!,
+                        style: const TextStyle(
+                            fontSize: 14,
+                            fontWeight: FontWeight.w600,
+                            color: AppColors.warning)),
+                  ],
+                ),
+              ))
+          .toList(),
+    );
+  }
+}

+ 192 - 0
lib/features/report/vehicle_detail_report_page.dart

@@ -0,0 +1,192 @@
+import 'package:flutter/material.dart';
+import 'package:fl_chart/fl_chart.dart';
+import '../../core/theme/app_colors.dart';
+
+class VehicleDetailReportPage extends StatelessWidget {
+  const VehicleDetailReportPage({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(title: const Text('用车明细表')),
+      body: SingleChildScrollView(
+        padding: const EdgeInsets.all(12),
+        child: Column(
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: [
+            _buildSummaryRow(),
+            const SizedBox(height: 16),
+            const Text('用车类型分布',
+                style: TextStyle(
+                    fontSize: 15,
+                    fontWeight: FontWeight.w600,
+                    color: AppColors.textPrimary)),
+            const SizedBox(height: 12),
+            SizedBox(
+              height: 220,
+              child: _buildPieChart(),
+            ),
+            const SizedBox(height: 16),
+            const Text('明细列表',
+                style: TextStyle(
+                    fontSize: 15,
+                    fontWeight: FontWeight.w600,
+                    color: AppColors.textPrimary)),
+            const SizedBox(height: 8),
+            _buildDetailList(),
+          ],
+        ),
+      ),
+    );
+  }
+
+  Widget _buildSummaryRow() {
+    return Row(
+      children: [
+        Expanded(
+          child: _summaryCard('总里程', '386 km', AppColors.primary),
+        ),
+        const SizedBox(width: 8),
+        Expanded(
+          child: _summaryCard('用车次数', '15 次', AppColors.success),
+        ),
+        const SizedBox(width: 8),
+        Expanded(
+          child: _summaryCard('预估费用', '¥1,158.00', AppColors.warning),
+        ),
+      ],
+    );
+  }
+
+  Widget _summaryCard(String label, String value, Color color) {
+    return Container(
+      padding: const EdgeInsets.all(12),
+      decoration: BoxDecoration(
+        color: color.withValues(alpha: 0.08),
+        borderRadius: BorderRadius.circular(10),
+      ),
+      child: Column(
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: [
+          Text(label,
+              style: const TextStyle(
+                  fontSize: 11, color: AppColors.textSecondary)),
+          const SizedBox(height: 4),
+          Text(value,
+              style: TextStyle(
+                  fontSize: 16,
+                  fontWeight: FontWeight.bold,
+                  color: color)),
+        ],
+      ),
+    );
+  }
+
+  Widget _buildPieChart() {
+    const data = [
+      ('商务车', 180.0, AppColors.primary),
+      ('轿车', 120.0, AppColors.success),
+      ('SUV', 56.0, AppColors.warning),
+      ('其他', 30.0, AppColors.textHint),
+    ];
+
+    return Row(
+      children: [
+        Expanded(
+          child: PieChart(
+            PieChartData(
+              sections: data.map((d) {
+                final (label, value, color) = d;
+                return PieChartSectionData(
+                  value: value,
+                  color: color,
+                  radius: 40,
+                  title: '${(value / 386 * 100).toStringAsFixed(0)}%',
+                  titleStyle: const TextStyle(
+                      fontSize: 10,
+                      fontWeight: FontWeight.bold,
+                      color: Colors.white),
+                );
+              }).toList(),
+              centerSpaceRadius: 30,
+              sectionsSpace: 2,
+            ),
+          ),
+        ),
+        const SizedBox(width: 12),
+        Column(
+          mainAxisAlignment: MainAxisAlignment.center,
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: data.map((d) {
+            final (label, value, color) = d;
+            return Padding(
+              padding: const EdgeInsets.symmetric(vertical: 2),
+              child: Row(
+                mainAxisSize: MainAxisSize.min,
+                children: [
+                  Container(
+                      width: 10,
+                      height: 10,
+                      decoration: BoxDecoration(
+                          color: color,
+                          borderRadius: BorderRadius.circular(2))),
+                  const SizedBox(width: 6),
+                  Text('$label ${value.toInt()}km',
+                      style: const TextStyle(
+                          fontSize: 11, color: AppColors.textSecondary)),
+                ],
+              ),
+            );
+          }).toList(),
+        ),
+      ],
+    );
+  }
+
+  Widget _buildDetailList() {
+    final items = [
+      {'date': '2024-05-05', 'applicant': '张三', 'type': '商务车', 'info': '客户接待'},
+      {'date': '2024-05-03', 'applicant': '李四', 'type': '轿车', 'info': '商务出行'},
+      {'date': '2024-04-28', 'applicant': '王五', 'type': 'SUV', 'info': '项目考察'},
+      {'date': '2024-04-25', 'applicant': '赵六', 'type': '轿车', 'info': '会议接送'},
+    ];
+    return Column(
+      children: items
+          .map((item) => Container(
+                margin: const EdgeInsets.only(bottom: 6),
+                padding: const EdgeInsets.all(12),
+                decoration: BoxDecoration(
+                  color: Colors.white,
+                  borderRadius: BorderRadius.circular(8),
+                  boxShadow: [
+                    BoxShadow(
+                        color: Colors.black.withValues(alpha: 0.04),
+                        blurRadius: 4,
+                        offset: const Offset(0, 1)),
+                  ],
+                ),
+                child: Row(
+                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                  children: [
+                    Column(
+                      crossAxisAlignment: CrossAxisAlignment.start,
+                      children: [
+                        Text('${item['applicant']} · ${item['type']}',
+                            style: const TextStyle(
+                                fontSize: 13,
+                                color: AppColors.textPrimary)),
+                        Text('${item['date']} · ${item['info']}',
+                            style: const TextStyle(
+                                fontSize: 10,
+                                color: AppColors.textHint)),
+                      ],
+                    ),
+                    const Icon(Icons.chevron_right,
+                        size: 18, color: AppColors.textHint),
+                  ],
+                ),
+              ))
+          .toList(),
+    );
+  }
+}

+ 38 - 2
lib/features/vehicle/vehicle_detail_page.dart

@@ -3,14 +3,50 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
 import '../../core/theme/app_colors.dart';
 import '../../core/utils/date_utils.dart' as du;
 import '../../core/utils/responsive.dart';
+import '../../shared/models/approval_status.dart';
 import '../../shared/widgets/form_section.dart';
 import '../../shared/widgets/form_field_row.dart';
 import '../../shared/widgets/status_tag.dart';
-import 'vehicle_api.dart';
 import 'vehicle_model.dart';
 
 final vehicleDetailProvider = FutureProvider.autoDispose.family<VehicleModel, String>((ref, id) async {
-  return ref.read(vehicleApiProvider).fetchDetail(id);
+  await Future.delayed(const Duration(milliseconds: 300));
+  return VehicleModel(
+    id: id,
+    applicationNo: 'VH202605001',
+    applicantId: 'u-001',
+    applicantName: '张三',
+    deptId: 'dept-001',
+    deptName: '市场部',
+    vehicleType: '轿车',
+    purpose: '客户拜访',
+    startTime: DateTime(2026, 5, 22, 8, 0),
+    endTime: DateTime(2026, 5, 22, 18, 0),
+    origin: '公司总部',
+    destination: '深圳分公司',
+    passengerCount: 3,
+    driver: '刘师傅',
+    licensePlate: '京A·88888',
+    estimatedMileage: 120.0,
+    estimatedCost: 500.0,
+    reason: '拜访重要客户,需前往深圳',
+    status: 'pending',
+    createTime: DateTime(2026, 5, 20),
+    updateTime: DateTime(2026, 5, 20),
+    approvalRecords: [
+      ApprovalRecord(
+        id: 'ar-vh-001',
+        bizId: id,
+        bizType: 'vehicle',
+        approverId: 'u-mgr',
+        approverName: '李四',
+        approvalLevel: 1,
+        action: 'pending',
+        opinion: '',
+        approvalTime: DateTime(2026, 5, 20),
+      ),
+    ],
+  );
 });
 
 class VehicleDetailPage extends ConsumerWidget {

+ 81 - 4
lib/features/vehicle/vehicle_list_controller.dart

@@ -1,14 +1,91 @@
 import 'package:flutter_riverpod/flutter_riverpod.dart';
+import '../../shared/models/approval_status.dart';
 import 'vehicle_model.dart';
-import 'vehicle_api.dart';
 
 final vehicleStatusFilterProvider = StateProvider<String>((ref) => '');
 
+final _mockVehicles = <VehicleModel>[
+  VehicleModel(
+    id: 'veh-001',
+    applicationNo: 'VH202605001',
+    applicantId: 'u-001',
+    applicantName: '张三',
+    deptId: 'dept-001',
+    deptName: '市场部',
+    vehicleType: '轿车',
+    purpose: '客户拜访',
+    startTime: DateTime(2026, 5, 22, 8, 0),
+    endTime: DateTime(2026, 5, 22, 18, 0),
+    origin: '公司总部',
+    destination: '深圳分公司',
+    passengerCount: 3,
+    driver: '刘师傅',
+    licensePlate: '京A·88888',
+    estimatedMileage: 120.0,
+    estimatedCost: 500.0,
+    reason: '拜访重要客户,需前往深圳',
+    status: 'pending',
+    createTime: DateTime(2026, 5, 20),
+    updateTime: DateTime(2026, 5, 20),
+    approvalRecords: [
+      ApprovalRecord(
+        id: 'ar-vh-001',
+        bizId: 'veh-001',
+        bizType: 'vehicle',
+        approverId: 'u-mgr',
+        approverName: '李四',
+        approvalLevel: 1,
+        action: 'pending',
+        opinion: '',
+        approvalTime: DateTime(2026, 5, 20),
+      ),
+    ],
+  ),
+  VehicleModel(
+    id: 'veh-002',
+    applicationNo: 'VH202605002',
+    applicantId: 'u-002',
+    applicantName: '王五',
+    deptId: 'dept-002',
+    deptName: '技术部',
+    vehicleType: 'SUV',
+    purpose: '设备运输',
+    startTime: DateTime(2026, 5, 25, 9, 0),
+    endTime: DateTime(2026, 5, 25, 17, 0),
+    origin: '公司仓库',
+    destination: '数据中心',
+    passengerCount: 2,
+    driver: '王师傅',
+    licensePlate: '京B·66666',
+    estimatedMileage: 60.0,
+    estimatedCost: 300.0,
+    reason: '运送服务器设备至数据中心',
+    status: 'approved',
+    createTime: DateTime(2026, 5, 23),
+    updateTime: DateTime(2026, 5, 24),
+    approvalRecords: [
+      ApprovalRecord(
+        id: 'ar-vh-002',
+        bizId: 'veh-002',
+        bizType: 'vehicle',
+        approverId: 'u-mgr',
+        approverName: '李四',
+        approvalLevel: 1,
+        action: 'approve',
+        opinion: '同意,注意安全',
+        approvalTime: DateTime(2026, 5, 24),
+      ),
+    ],
+  ),
+];
+
 final vehicleListProvider =
     FutureProvider.autoDispose.family<List<VehicleModel>, int>(
         (ref, page) async {
+  await Future.delayed(const Duration(milliseconds: 300));
   final status = ref.watch(vehicleStatusFilterProvider);
-  final api = ref.watch(vehicleApiProvider);
-  final result = await api.fetchList(status: status, page: page);
-  return result.list;
+  if (status.isEmpty) {
+    return _mockVehicles;
+  }
+  return _mockVehicles.where((e) => e.status == status).toList();
 });