【HarmonyOS】Flutter实战项目+校园通服务平台全解

【HarmonyOS】Flutter实战项目+校园通服务平台全解

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

摘要

本文详细指导开发者从零开始构建"校园通"校园服务平台,涵盖课表查询、校园卡余额、失物招领、NFC模拟等核心功能。文章以Flutter为主UI框架,通过Platform Channel深度集成华为Push Kit、NFC和地图定位等鸿蒙原生能力,实现原子化服务卡片。提供完整项目结构、核心代码示例及上架鸿蒙应用市场的合规要点,帮助开发者快速掌握Flutter+HarmonyOS混合开发技能。


一、项目概述

1.1 应用架构设计

复制代码
┌─────────────────────────────────────────────────────────────────────┐
│                    "校园通"应用整体架构                             │
├─────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │                    Flutter UI层                            │   │
│  │                                                             │   │
│  │  ┌─────────┐  ┌─────────┐  ┌─────────┐  ┌─────────┐  │   │
│  │  │课表页面  │  │校园卡   │  │失物招领 │  │NFC模拟  │  │   │
│  │  └─────────┘  └─────────┘  └─────────┘  └─────────┘  │   │
│  │                                                             │   │
│  │  ┌─────────┐  ┌─────────┐  ┌─────────┐              │   │
│  │  │地图服务  │  │消息中心  │  │个人中心  │              │   │
│  │  └─────────┘  └─────────┘  └─────────┘              │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                              │                                       │
│                              ▼                                       │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │                  业务逻辑层 (Dart)                             │   │
│  │                                                             │   │
│  │  ┌──────────┐  ┌──────────┐  ┌──────────┐          │   │
│  │  │课表Service│  │卡片Service│  │NFCService │          │   │
│  │  └──────────┘  └──────────┘  └──────────┘          │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                              │                                       │
│                              ▼                                       │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │               Platform Channel 桥接层                     │   │
│  │                                                             │   │
│  │  MethodChannel: 'com.campus.push'                         │   │
│  │  MethodChannel: 'com.campus.nfc'                          │   │
│  │  MethodChannel: 'com.campus.location'                       │   │
│  │  MethodChannel: 'com.campus.map'                            │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                              │                                       │
│                              ▼                                       │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │               HarmonyOS Native Layer (ArkTS)                 │   │
│  │                                                             │   │
│  │  ┌──────────┐  ┌──────────┐  ┌──────────┐          │   │
│  │  │Push Kit  │  │NFC Kit   │  │Map Kit   │          │   │
│  │  │集成      │  │集成      │  │集成      │          │   │
│  │  └──────────┘  └──────────┘  └──────────┘          │   │
│  └─────────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────┘

1.2 核心功能模块

功能模块 说明 技术实现
课表查询 展示本周课程安排,支持课程提醒 原子化服务卡片 + 定时提醒
校园卡余额 查询饭卡余额、消费记录 Platform Channel + 卡包服务
失物招领 发布和查找失物信息 地图定位 + 图片上传
NFC模拟 模拟NFC刷卡功能 NFC Kit + 原子化服务
消息推送 接收校园通知、课程提醒 Push Kit
地图服务 校园导航、失物位置标注 Map Kit

1.3 技术栈选型

yaml 复制代码
# pubspec.yaml
dependencies:
  # UI框架
  flutter:
    sdk: flutter

  # 状态管理
  flutter_riverpod: ^2.6.1
  riverpod_annotation: ^2.6.1

  # 网络请求
  dio: ^5.7.0

  # 本地存储
  hive: ^2.2.3
  hive_flutter: ^1.1.0
  shared_preferences: ^2.3.3

  # UI组件
  flutter_tab_icon_v2: ^0.0.1
  cached_network_image: ^3.4.1
  flutter_slidable: ^3.1.1

  # 地图与定位
  amap_flutter_map: ^3.0.0
  amap_flutter_location: ^3.0.0

  # NFC
  flutter_nfc_kit: ^3.0.0

  # 日期处理
  intl: ^0.19.0
  table_calendar: ^3.1.2

  # 鸿蒙适配
  flutter_ohos: ^1.0.0

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^4.0.0
  riverpod_generator: ^2.6.1
  riverpod_lint: ^2.6.1
  build_runner: ^2.4.13

二、项目结构设计

2.1 完整目录结构

复制代码
campus_pass/
├── lib/
│   ├── main.dart                          # 应用入口
│   │
│   ├── app.dart                           # 根组件
│   │
│   ├── core/                             # 核心层
│   │   ├── constants/                     # 常量定义
│   │   │   ├── api_constants.dart
│   │   │   └── app_constants.dart
│   │   ├── theme/                         # 主题配置
│   │   │   ├── app_theme.dart
│   │   │   └── app_colors.dart
│   │   ├── router/                        # 路由配置
│   │   │   └── app_router.dart
│   │   ├── utils/                         # 工具类
│   │   │   ├── logger.dart
│   │   │   ├── validators.dart
│   │   │   └── date_utils.dart
│   │   └── widgets/                       # 通用组件
│   │       ├── app_card.dart               # 原子化服务卡片
│   │       ├── app_button.dart
│   │       ├── app_input.dart
│   │       └── loading_shimmer.dart
│   │
│   ├── data/                             # 数据层
│   │   ├── models/                        # 数据模型
│   │   │   ├── course.dart                 # 课程模型
│   │   │   ├── campus_card.dart            # 校园卡模型
│   │   │   ├── lost_item.dart              # 失物模型
│   │   │   └── notification.dart           # 通知模型
│   │   │
│   │   ├── repositories/                  # 仓储实现
│   │   │   ├── course_repository.dart
│   │   │   ├── card_repository.dart
│   │   │   └── lost_item_repository.dart
│   │   │
│   │   ├── datasources/                   # 数据源
│   │       ├── remote/                    # 远程数据源
│   │       │   ├── api_service.dart
│   │       │   ├── course_remote_ds.dart
│   │       │   └── card_remote_ds.dart
│   │       │
│   │       └── local/                     # 本地数据源
│   │           ├── app_database.dart       # Hive数据库
│   │           └── app_preferences.dart    # SharedPreferences
│   │
│   ├── domain/                           # 领域层
│   │   ├── entities/                     # 领域实体
│   │   └── usecases/                    # 用例
│   │       ├── get_courses_usecase.dart
│   │       ├── query_balance_usecase.dart
│   │       └── report_lost_item_usecase.dart
│   │
│   └── presentation/                    # 表示层
│       ├── providers/                     # 状态管理
│       │   ├── auth_provider.dart
│       │   ├── course_provider.dart
│       │   ├── card_provider.dart
│       │   └── notification_provider.dart
│       │
│       ├── pages/                         # 页面
│       │   ├── home/                       # 首页
│       │   │   └── home_page.dart
│       │   ├── schedule/                   # 课表页
│       │   │   └── schedule_page.dart
│       │   ├── campus_card/                 # 校园卡页
│       │   │   └── campus_card_page.dart
│       │   ├── lost_found/                  # 失物招领页
│       │   │   └── lost_found_page.dart
│       │   ├── nfc_simulator/              # NFC模拟页
│       │   │   └── nfc_simulator_page.dart
│       │   └── map/                       # 地图页
│       │       └── campus_map_page.dart
│       │
│       └── widgets/                       # 页面专用组件
│           ├── schedule/                   # 课表组件
│           │   ├── course_card.dart
│           │   └── week_calendar.dart
│           ├── card/                       # 校园卡组件
│           │   ├── balance_card.dart
│           │   └── transaction_item.dart
│           └── lost_found/                 # 失物组件
│               ├── lost_item_card.dart
│               └── publish_dialog.dart
│
├── ohos/                                 # 鸿蒙原生代码
│   └── entry/
│       └── src/main/
│           ├── ets/                        # ArkTS代码
│           │   ├── plugins/                 # Plugin封装
│           │   │   ├── PushKitPlugin.ts
│           │   │   ├── NFCKitPlugin.ts
│           │   │   └── MapKitPlugin.ts
│           │   ├── services/                # 服务层
│           │   │   ├── PushService.ts
│           │   │   ├── NFCService.ts
│           │   │   └── LocationService.ts
│           │   └── entryability/            # Ability
│           └── resources/                 # 资源文件
│               ├── base/
│               │   └── element/
│               │       ├── string.json       # 字符串资源
│               └── rawfile/                 # 原始文件
│
├── android/                              # Android平台代码
├── ios/                                  # iOS平台代码
├── web/                                  # Web平台代码
├── windows/                              # Windows平台代码
├── test/                                 # 测试代码
├── pubspec.yaml                          # 依赖配置
├── build-profile.json5                    # 构建配置
└── README.md                              # 项目说明

三、Platform Channel桥接实现

3.1 推送服务集成

Dart端:PushService

dart 复制代码
// lib/data/services/push_service.dart

import 'package:flutter/services.dart';

/// 推送服务
class PushService {
  static const MethodChannel _channel = MethodChannel('com.campus.push');
  static const EventChannel _eventChannel = EventChannel('com.campus.push_events');

  Stream<Map<String, dynamic>>? _notificationStream;

  /// 初始化推送服务
  Future<bool> initialize({
    required String appId,
  }) async {
    try {
      final result = await _channel.invokeMethod<bool>('initialize', {
        'appId': appId,
      });
      return result ?? false;
    } on PlatformException catch (e) {
      throw PushException('初始化失败: ${e.message}');
    }
  }

  /// 请求推送权限
  Future<bool> requestPermission() async {
    try {
      final result = await _channel.invokeMethod<bool>('requestPermission');
      return result ?? false;
    } on PlatformException catch (e) {
      throw PushException('请求权限失败: ${e.message}');
    }
  }

  /// 获取Token
  Future<String?> getToken() async {
    try {
      final result = await _channel.invokeMethod<String>('getToken');
      return result;
    } on PlatformException catch (e) {
      throw PushException('获取Token失败: ${e.message}');
    }
  }

  /// 订阅主题
  Future<bool> subscribeToTopic(String topic) async {
    try {
      final result = await _channel.invokeMethod<bool>('subscribeToTopic', {
        'topic': topic,
      });
      return result ?? false;
    } on PlatformException catch (e) {
      throw PushException('订阅主题失败: ${e.message}');
    }
  }

  /// 取消订阅
  Future<bool> unsubscribeFromTopic(String topic) async {
    try {
      final result = await _channel.invokeMethod<bool>('unsubscribeFromTopic', {
        'topic': topic,
      });
      return result ?? false;
    } on PlatformException catch (e) {
      throw PushException('取消订阅失败: ${e.message}');
    }
  }

  /// 监听推送消息
  Stream<Map<String, dynamic>> get notificationStream {
    _notificationStream ??= _eventChannel.receiveBroadcastStream().map(
      (dynamic event) {
        final Map<String, dynamic> data = event as Map<String, dynamic>;
        return {
          'title': data['title'] as String? ?? '',
          'content': data['content'] as String? ?? '',
          'extra': data['extra'] as Map<String, dynamic>? ?? {},
        };
      },
    );
    return _notificationStream!;
  }

  /// 本地通知
  Future<bool> showLocalNotification({
    required String title,
    required String content,
    Map<String, dynamic>? payload,
  }) async {
    try {
      final result = await _channel.invokeMethod<bool>('showLocalNotification', {
        'title': title,
        'content': content,
        'payload': payload,
      });
      return result ?? false;
    } on PlatformException catch (e) {
      throw PushException('显示通知失败: ${e.message}');
    }
  }

  /// 设置通知渠道
  Future<bool> createNotificationChannel({
    required String channelId,
    required String channelName,
    String description = '',
  }) async {
    try {
      final result = await _channel.invokeMethod<bool>('createNotificationChannel', {
        'channelId': channelId,
        'channelName': channelName,
        'description': description,
      });
      return result ?? false;
    } on PlatformException catch (e) {
      throw PushException('创建通知渠道失败: ${e.message}');
    }
  }
}

/// 推送异常
class PushException implements Exception {
  final String message;
  PushException(this.message);

  @override
  String toString() => message;
}

鸿蒙原生端:PushKitPlugin

typescript 复制代码
// ohos/entry/src/main/ets/plugins/PushKitPlugin.ts

import pushService from '@ohos.pushKit';
import { MethodCall, MethodCallHandler, MethodResult, MethodChannel } from '@ohos/flutter_ohos';

/**
 * 华为Push Kit Plugin
 * 实现Flutter与鸿蒙推送服务的桥接
 */
export class PushKitPlugin implements MethodCallHandler {
  private static readonly CHANNEL_NAME = 'com.campus.push';

  private pushService: pushService.PushService;
  private notificationId = 0;

  /**
   * 注册Plugin到Flutter Engine
   */
  static registerWith(flutterEngine: any): void {
    const channel = new MethodChannel(
      flutterEngine.dartExecutor.binaryMessenger,
      PushKitPlugin.CHANNEL_NAME
    );

    const plugin = new PushKitPlugin();
    channel.setMethodCallHandler(plugin);
  }

  constructor() {
    this.pushService = new pushService.PushService();
  }

  /**
   * 处理来自Dart的方法调用
   */
  async onMethodCall(call: MethodCall, result: MethodResult): Promise<void> {
    switch (call.method) {
      case 'initialize':
        await this.handleInitialize(call, result);
        break;
      case 'requestPermission':
        await this.handleRequestPermission(call, result);
        break;
      case 'getToken':
        await this.handleGetToken(call, result);
        break;
      case 'subscribeToTopic':
        await this.handleSubscribeToTopic(call, result);
        break;
      case 'unsubscribeFromTopic':
        await this.handleUnsubscribeFromTopic(call, result);
        break;
      case 'showLocalNotification':
        await this.handleShowLocalNotification(call, result);
        break;
      case 'createNotificationChannel':
        await this.handleCreateNotificationChannel(call, result);
        break;
      default:
        result.notImplemented();
    }
  }

  /**
   * 初始化Push服务
   */
  private async handleInitialize(call: MethodCall, result: MethodResult): Promise<void> {
    try {
      const appId: string = call.args()['appId'];

      // 配置Push服务
      const pushOption: pushService.PushOption = {
        appId: appId,
        // 启用调试模式(生产环境设为false)
        debug: false,
        // 设置通知渠道
        notificationChannelOption: {
          // 课程提醒渠道
          courseReminder: {
            channelName: '课程提醒',
            channelDescription: '用于推送课程开始提醒',
            channelLevel: pushService.NotificationLevel.LEVEL_HIGH,
            lockscreenVisibility: pushService.LockscreenVisibility.PUBLIC,
            sound: 'course_reminder.mp3',
            light: true,
            vibration: true,
          },
          // 校园通知渠道
          campusNotification: {
            channelName: '校园通知',
            channelDescription: '用于推送校园重要通知',
            channelLevel: pushService.NotificationLevel.LEVEL_DEFAULT,
            lockscreenVisibility: pushService.LockscreenVisibility.PUBLIC,
            sound: 'default',
            vibration: true,
          },
        },
      };

      await this.pushService.init(pushOption);

      // 注册通知监听器
      this.pushService.on('notification', (data: pushService.PushData) => {
        this._sendNotificationToDart(data);
      });

      // 注册Token监听器
      this.pushService.on('token', (data: pushService.PushToken) => {
        this._sendTokenToDart(data);
      });

      result.success(true);
    } catch (error) {
      result.error('INIT_FAILED', `初始化Push服务失败: ${error.message}`, null);
    }
  }

  /**
   * 请求推送权限
   */
  private async handleRequestPermission(call: MethodCall, result: MethodResult): Promise<void> {
    try {
      const permission = await this.pushService.requestPermission(
        pushService.NotificationPermission.NOTIFICATION,
        {
          reason: '需要推送权限以接收课程提醒和校园通知',
        }
      );

      result.success(permission === pushService.AuthorizedResult.ALLOWED);
    } catch (error) {
      result.error('PERMISSION_DENIED', `请求权限失败: ${error.message}`, null);
    }
  }

  /**
   * 获取推送Token
   */
  private async handleGetToken(call: MethodCall, result: MethodResult): Promise<void> {
    try {
      const token = await this.pushService.getToken();
      result.success(token);
    } catch (error) {
      result.error('GET_TOKEN_FAILED', `获取Token失败: ${error.message}`, null);
    }
  }

  /**
   * 订阅主题
   */
  private async handleSubscribeToTopic(call: MethodCall, result: MethodResult): Promise<void> {
    try {
      const topic: string = call.args()['topic'];

      await this.pushService.subscribe(topic);

      result.success(true);
    } catch (error) {
      result.error('SUBSCRIBE_FAILED', `订阅主题失败: ${error.message}`, null);
    }
  }

  /**
   * 取消订阅主题
   */
  private async handleUnsubscribeFromTopic(call: MethodCall, result: MethodResult): Promise<void> {
    try {
      const topic: string = call.args()['topic'];

      await this.pushService.unsubscribe(topic);

      result.success(true);
    } catch (error) {
      result.error('UNSUBSCRIBE_FAILED', `取消订阅失败: ${error.message}`, null);
    }
  }

  /**
   * 显示本地通知
   */
  private async handleShowLocalNotification(call: MethodCall, result: MethodResult): Promise<void> {
    try {
      const title: string = call.args()['title'];
      const content: string = call.args()['content'];
      const payload: ESObject = call.args()['payload'] || {};

      const notificationRequest: pushService.NotificationRequest = {
        id: this.notificationId++,
        content: {
          notificationContentType: pushService.NotificationContentType.NOTIFICATION_CONTENT_BASIC_TEXT,
          notificationCategory: pushService.NotificationCategory.SOCIAL_COMMUNICATION,
          title: title,
          text: content,
        },
        notificationSlotType: pushService.NotificationSlotType.SOCIAL_COMMUNICATION,
        // 自定义点击动作
        actionButtons: [
          {
            title: '查看详情',
            intent: {
              packageName: 'com.campus.pass',
              abilityName: 'EntryAbility',
            },
          },
        ],
      };

      await this.pushService.publish(notificationRequest);

      result.success(true);
    } catch (error) {
      result.error('SHOW_NOTIFICATION_FAILED', `显示通知失败: ${error.message}`, null);
    }
  }

  /**
   * 创建通知渠道
   */
  private async handleCreateNotificationChannel(call: MethodCall, result: MethodResult): Promise<void> {
    try {
      const channelId: string = call.args()['channelId'];
      const channelName: string = call.args()['channelName'];
      const description: string = call.args()['description'];

      // 鸿蒙通知渠道已在init中配置
      result.success(true);
    } catch (error) {
      result.error('CREATE_CHANNEL_FAILED', `创建通知渠道失败: ${error.message}`, null);
    }
  }

  /**
   * 发送通知到Dart层
   */
  private _sendNotificationToDart(data: pushService.PushData): void {
    const notificationData = {
      title: data.notification?.title || '',
      content: data.notification?.content || '',
      extra: data.payload || {},
    };

    // 通过EventChannel发送
    // eventChannel.emit('com.campus.push_events/notification', notificationData);
  }

  /**
   * 发送Token到Dart层
   */
  private _sendTokenToDart(data: pushService.PushToken): void {
    const tokenData = {
      token: data.token || '',
      appId: data.appId || '',
    };

    // 保存Token到本地或发送到服务器
    console.log('Push Token:', tokenData);
  }
}

3.2 NFC服务集成

Dart端:NFCService

dart 复制代码
// lib/data/services/nfc_service.dart

import 'package:flutter/services.dart';

/// NFC数据类型
enum NFCDataType {
  campusCard,
  doorAccess,
  library,
}

/// NFC读取结果
class NFCReadResult {
  final String data;
  final NFCDataType dataType;
  final DateTime timestamp;

  NFCReadResult({
    required this.data,
    required this.dataType,
    required this.timestamp,
  });

/// NFC服务
class NFCService {
  static const MethodChannel _channel = MethodChannel('com.campus.nfc');

  /// 初始化NFC
  Future<bool> initialize() async {
    try {
      final result = await _channel.invokeMethod<bool>('initialize');
      return result ?? false;
    } on PlatformException catch (e) {
      throw NFCException('初始化NFC失败: ${e.message}');
    }
  }

  /// 检查NFC是否可用
  Future<bool> isAvailable() async {
    try {
      final result = await _channel.invokeMethod<bool>('isAvailable');
      return result ?? false;
    } on PlatformException catch (e) {
      throw NFCException('检查NFC可用性失败: ${e.message}');
    }
  }

  /// 开始读取NFC标签
  Future<NFCReadResult?> startReading() async {
    try {
      final result = await _channel.invokeMethod<Map<String, dynamic>>('startReading');

      if (result == null) return null;

      return NFCReadResult(
        data: result['data'] as String,
        dataType: _stringToNFCType(result['type'] as String? ?? ''),
        timestamp: DateTime.now(),
      );
    } on PlatformException catch (e) {
      throw NFCException('读取NFC失败: ${e.message}');
    }
  }

  /// 停止读取NFC
  Future<void> stopReading() async {
    try {
      await _channel.invokeMethod('stopReading');
    } on PlatformException catch (e) {
      throw NFCException('停止读取失败: ${e.message}');
    }
  }

  /// 写入NFC数据
  Future<bool> writeData({
    required String data,
    required NFCDataType dataType,
  }) async {
    try {
      final result = await _channel.invokeMethod<bool>('writeData', {
        'data': data,
        'type': _nfcTypeToString(dataType),
      });
      return result ?? false;
    } on PlatformException catch (e) {
      throw NFCException('写入NFC失败: ${e.message}');
    }
  }

  /// 模拟NFC刷卡(用于测试)
  Future<NFCReadResult> simulateCardSwipe({
    required String cardId,
    required double balance,
  }) async {
    // 模拟NFC读取延迟
    await Future.delayed(const Duration(milliseconds: 500));

    return NFCReadResult(
      data: cardId,
      dataType: NFCDataType.campusCard,
      timestamp: DateTime.now(),
    );
  }

  String _nfcTypeToString(NFCDataType type) {
    switch (type) {
      case NFCDataType.campusCard:
        return 'campus_card';
      case NFCDataType.doorAccess:
        return 'door_access';
      case NFCDataType.library:
        return 'library';
    }
  }

  NFCDataType _stringToNFCType(String type) {
    switch (type) {
      case 'campus_card':
        return NFCDataType.campusCard;
      case 'door_access':
        return NFCDataType.doorAccess;
      case 'library':
        return NFCDataType.library;
      default:
        return NFCDataType.campusCard;
    }
  }
}

/// NFC异常
class NFCException implements Exception {
  final String message;
  NFCException(this.message);

  @override
  String toString() => message;
}

鸿蒙原生端:NFCKitPlugin

typescript 复制代码
// ohos/entry/src/main/ets/plugins/NFCKitPlugin.ts

import { MethodCall, MethodCallHandler, MethodResult, MethodChannel } from '@ohos/flutter_ohos';
import nfcController from '@ohos.nfc.controller';
import { NfcTech } from '@ohos.nfc.tag';

/**
 * NFC Kit Plugin
 * 实现Flutter与鸿蒙NFC服务的桥接
 */
export class NFCKitPlugin implements MethodCallHandler {
  private static readonly CHANNEL_NAME = 'com.campus.nfc';

  private nfcController: nfcController.NfcController;
  private isReading = false;

  /**
   * 注册Plugin到Flutter Engine
   */
  static registerWith(flutterEngine: any): void {
    const channel = new MethodChannel(
      flutterEngine.dartExecutor.binaryMessenger,
      NFCKitPlugin.CHANNEL_NAME
    );

    const plugin = new NFCKitPlugin();
    channel.setMethodCallHandler(plugin);
  }

  constructor() {
    // 创建NFC控制器
    this.nfcController = new nfcController.NfcController();
  }

  /**
   * 处理来自Dart的方法调用
   */
  async onMethodCall(call: MethodCall, result: MethodResult): Promise<void> {
    switch (call.method) {
      case 'initialize':
        await this.handleInitialize(call, result);
        break;
      case 'isAvailable':
        await this.handleIsAvailable(call, result);
        break;
      case 'startReading':
        await this.handleStartReading(call, result);
        break;
      case 'stopReading':
        await this.handleStopReading(call, result);
        break;
      case 'writeData':
        await this.handleWriteData(call, result);
        break;
      default:
        result.notImplemented();
    }
  }

  /**
   * 初始化NFC控制器
   */
  private async handleInitialize(call: MethodCall, result: MethodResult): Promise<void> {
    try {
      // 配置NFC技术类型
      const techTypes: Array<NfcTech> = [
        NfcTech.ISO_DEP,
        NfcTech.ISO_IClassA,
        NfcTech.ISO_IClassB,
        NfcTech.MIFARE_CLASSIC,
      ];

      // 初始化NFC控制器
      await this.nfcController.init(techTypes);

      result.success(true);
    } catch (error) {
      result.error('INIT_FAILED', `初始化NFC失败: ${error.message}`, null);
    }
  }

  /**
   * 检查NFC是否可用
   */
  private async handleIsAvailable(call: MethodCall, result: MethodResult): Promise<void> {
    try {
      const available = await this.nfcController.isNfcAvailable();
      result.success(available);
    } catch (error) {
      result.error('CHECK_FAILED', `检查NFC失败: ${error.message}`, null);
    }
  }

  /**
   * 开始读取NFC标签
   */
  private async handleStartReading(call: MethodCall, result: MethodResult): Promise<void> {
    if (this.isReading) {
      result.success(null);
      return;
    }

    try {
      this.isReading = true;

      // 注册NFC标签监听器
      this.nfcController.on('discovered', (tagInfo) => {
        this._processNfcTag(tagInfo, result);
      });

      // 开始轮询NFC标签
      const flags: Array<nfcController.DiscoveredFilter> = [
        nfcController.DiscoveredFilter.NDEF_TECH,
        nfcController.DiscoveredFilter.NDEF_FORMAT,
        nfcController.DiscoveredFilter.NDEF_FORUM_NDEF,
      ];

      const discoveredFilter: nfcController.DiscoveredFilter = {
        tech: [NfcTech.ISO_DEP],
      };

      await this.nfcController.on(discoveredFilter);
    } catch (error) {
      this.isReading = false;
      result.error('READ_FAILED', `开始读取失败: ${error.message}`, null);
    }
  }

  /**
   * 停止读取NFC标签
   */
  private async handleStopReading(call: MethodCall, result: MethodResult): Promise<void> {
    try {
      this.isReading = false;
      await this.nfcController.off();
      result.success(true);
    } catch (error) {
      result.error('STOP_FAILED', `停止读取失败: ${error.message}`, null);
    }
  }

  /**
   * 处理NFC标签数据
   */
  private async _processNfcTag(
    tagInfo: nfcController.TagInfo,
    result: MethodResult
  ): Promise<void> {
    try {
      const tag = tagInfo.tag;

      // 读取NDEF数据
      const ndefMessage = await tag.getNdefMessage();

      if (ndefMessage == null || ndefMessage.records.length === 0) {
        result.success({
          data: '',
          type: 'unknown',
        });
        return;
      }

      // 解析NDEF记录
      const record = ndefMessage.records[0];
      const payload = record.payload;

      // 根据payload类型解析数据
      let nfcData: ESObject = {
        data: '',
        type: 'campus_card',
      };

      if (payload instanceof ArrayBuffer) {
        const textDecoder = new TextDecoder('utf-8');
        const text = textDecoder.decode(payload);
        nfcData.data = text;

        // 判断卡片类型
        if (text.startsWith('CARD_')) {
          nfcData.type = 'campus_card';
        } else if (text.startsWith('DOOR_')) {
          nfcData.type = 'door_access';
        } else if (text.startsWith('LIB_')) {
          nfcData.type = 'library';
        }
      }

      result.success(nfcData);
      this.isReading = false;
    } catch (error) {
      this.isReading = false;
      result.error('PROCESS_FAILED', `处理NFC数据失败: ${error.message}`, null);
    }
  }

  /**
   * 写入NFC数据
   */
  private async handleWriteData(call: MethodCall, result: MethodResult): Promise<void> {
    try {
      const data: string = call.args()['data'];
      const type: string = call.args()['type'];

      // 创建NDEF记录
      const textEncoder = new TextEncoder();
      const payload = textEncoder.encodeInto(data);

      const ndefRecord: nfcController.NdefRecord = {
        id: new Uint8Array([]),
        tnf: nfcController.TNF_WELL_KNOWN,
        type: new Uint8Array([0x54]), // Text类型
        payload: payload,
      };

      const ndefMessage: nfcController.NdefMessage = {
        records: [ndefRecord],
      };

      // 写入NFC标签(需要标签靠近)
      result.success({
        message: '请将NFC标签靠近设备',
        ndefMessage: ndefMessage,
      });
    } catch (error) {
      result.error('WRITE_FAILED', `写入NFC数据失败: ${error.message}`, null);
    }
  }
}

3.3 地图定位服务集成

Dart端:LocationService

dart 复制代码
// lib/data/services/location_service.dart

import 'package:flutter/material.dart';
import 'package:amap_flutter_location/amap_flutter_location.dart';

/// 位置信息模型
class LocationInfo {
  final double latitude;
  final double longitude;
  final String address;
  final DateTime timestamp;

  LocationInfo({
    required this.latitude,
    required this.longitude,
    required this.address,
    required this.timestamp,
  });

/// 定位服务
class LocationService {
  static final AmapFlutterLocation _locationPlugin = AmapFlutterLocation();
  Stream<LatLng>? _locationStream;

  /// 初始化定位服务
  Future<bool> initialize({
    required String iosKey,
    required String androidKey,
  }) async {
    try {
      // 配置高德地图Key
      AmapFlutterLocation.setApiKey(iosKey: iosKey, androidKey: androidKey);

      // 设置隐私协议
      AmapFlutterLocation.updatePrivacyShow(false);
      AmapFlutterLocation.updatePrivacyAgree(true);

      // 设置定位参数
      await AmapFlutterLocation.setLocationOption(
        AmapLocationOption(
          // 定位模式
          locationMode: AmapLocationMode.hightAccuracy,
          // 定位间隔
          locationInterval: 2000,
          // 返回地址信息
          needAddress: true,
          // 一次定位
          onceLocation: false,
          // 位置过滤
          gpsFirst: true,
          // 超时时间
          httpTimeOut: 20000,
        ),
      );

      return true;
    } catch (e) {
      debugPrint('初始化定位服务失败: $e');
      return false;
    }
  }

  /// 请求定位权限
  Future<bool> requestPermission() async {
    try {
      final result = await AmapFlutterLocation.requestPermission();
      return result?.contains(Permission.permissionAlways) ?? false;
    } catch (e) {
      debugPrint('请求定位权限失败: $e');
      return false;
    }
  }

  /// 获取当前位置
  Future<LocationInfo?> getCurrentLocation() async {
    try {
      final result = await AmapFlutterLocation.getLocation();

      if (result == null) return null;

      return LocationInfo(
        latitude: result.latitude,
        longitude: result.longitude,
        address: result.formattedAddress ?? '未知位置',
        timestamp: DateTime.now(),
      );
    } catch (e) {
      debugPrint('获取当前位置失败: $e');
      return null;
    }
  }

  /// 开始连续定位
  Stream<LocationInfo> startLocationStream() {
    _locationStream ??= AmapFlutterLocation.onLocationChanged().map((location) {
      return LocationInfo(
        latitude: location.latitude,
        longitude: location.longitude,
        address: location.formattedAddress ?? '未知位置',
        timestamp: DateTime.now(),
      );
    });

    return _locationStream!;
  }

  /// 停止定位
  Future<void> stopLocation() async {
    try {
      await AmapFlutterLocation.stopLocate();
    } catch (e) {
      debugPrint('停止定位失败: $e');
    }
  }

  /// 计算两点距离(单位:米)
  double calculateDistance({
    required double startLat,
    required double startLng,
    required double endLat,
    required double endLng,
  }) {
    const double earthRadius = 6371000; // 地球半径,单位:米
    final double dLat = _degreesToRadians(endLat - startLat);
    final double dLng = _degreesToRadians(endLng - startLng);

    final double a = (dLat / 2).abs() *
        (dLat / 2).abs() +
        (dLng / 2).abs() *
        (dLng / 2).abs() *
        (dLng / 2).abs() *
        startLat.cos() *
        endLat.cos();

    final double c = 2 * (a.sqrt() * (1 - a).sqrt()).asin();
    return earthRadius * c;
  }

  double _degreesToRadians(double degrees) {
    return degrees * (3.14159265359 / 180);
  }
}

四、原子化服务卡片实现

4.1 原子化服务卡片基础组件

dart 复制代码
// lib/core/widgets/app_card.dart

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

/// 原子化服务卡片配置
class ServiceCardConfig {
  final String id;
  final String title;
  final String description;
  final IconData icon;
  final Color iconColor;
  final String? route;
  final VoidCallback? onTap;
  final bool isPinned;
  final int badgeCount;

  const ServiceCardConfig({
    required this.id,
    required this.title,
    required this.description,
    required this.icon,
    required this.iconColor,
    this.route,
    this.onTap,
    this.isPinned = false,
    this.badgeCount = 0,
  });
}

/// 原子化服务卡片Widget
class ServiceCard extends StatelessWidget {
  final ServiceCardConfig config;
  final VoidCallback? onPinToggle;
  final VoidCallback? onReorder;

  const ServiceCard({
    super.key,
    required this.config,
    this.onPinToggle,
    this.onReorder,
  });

  @override
  Widget build(BuildContext context) {
    return Container(
      margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(16),
        boxShadow: [
          BoxShadow(
            color: Colors.black.withOpacity(0.05),
            blurRadius: 10,
            offset: const Offset(0, 2),
          ),
        ],
      ),
      child: Material(
        color: Colors.transparent,
        child: InkWell(
          onTap: config.onTap ?? () => _handleTap(context),
          borderRadius: BorderRadius.circular(16),
          child: Padding(
            padding: const EdgeInsets.all(16),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                // 头部:图标、标题和固定按钮
                Row(
                  children: [
                    // 图标
                    Container(
                      width: 48,
                      height: 48,
                      decoration: BoxDecoration(
                        color: config.iconColor.withOpacity(0.1),
                        borderRadius: BorderRadius.circular(12),
                      ),
                      child: Icon(
                        config.icon,
                        color: config.iconColor,
                        size: 24,
                      ),
                    ),
                    const SizedBox(width: 12),

                    // 标题和徽章
                    Expanded(
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          Row(
                            children: [
                              Expanded(
                                child: Text(
                                  config.title,
                                  style: const TextStyle(
                                    fontSize: 16,
                                    fontWeight: FontWeight.w600,
                                  ),
                                  maxLines: 1,
                                  overflow: TextOverflow.ellipsis,
                                ),
                              ),
                              if (config.badgeCount > 0)
                                Container(
                                  padding: const EdgeInsets.symmetric(
                                    horizontal: 8,
                                    vertical: 2,
                                  ),
                                  decoration: BoxDecoration(
                                    color: Colors.red,
                                    borderRadius: BorderRadius.circular(10),
                                  ),
                                  child: Text(
                                    '${config.badgeCount}',
                                    style: const TextStyle(
                                      color: Colors.white,
                                      fontSize: 12,
                                      fontWeight: FontWeight.w600,
                                    ),
                                  ),
                                ),
                            ],
                          ),
                          Text(
                            config.description,
                            style: TextStyle(
                              fontSize: 12,
                              color: Colors.grey[600],
                            ),
                            maxLines: 2,
                            overflow: TextOverflow.ellipsis,
                          ),
                        ],
                      ),
                    ),

                    // 固定按钮
                    if (onPinToggle != null)
                      IconButton(
                        icon: Icon(
                          config.isPinned
                              ? Icons.push_pin
                              : Icons.push_pin_outlined,
                          color: config.isPinned
                              ? Colors.blue
                              : Colors.grey[400],
                        ),
                        onPressed: onPinToggle,
                        padding: EdgeInsets.zero,
                        constraints: const BoxConstraints(
                          minWidth: 40,
                          minHeight: 40,
                        ),
                      ),
                  ],
                ),

                const SizedBox(height: 12),

                // 快捷操作区(可扩展)
                if (config.id == 'campus_card')
                  _buildCampusCardActions(context)
                else if (config.id == 'schedule')
                  _buildScheduleActions(context)
                else if (config.id == 'lost_found')
                  _buildLostFoundActions(context),
              ],
            ),
          ),
        ),
      ),
    );
  }

  Widget _buildCampusCardActions(BuildContext context) {
    return Row(
      children: [
        _buildActionButton(
          icon: Icons.account_balance_wallet_outlined,
          label: '余额',
          color: Colors.blue,
          onTap: () => _showBalance(context),
        ),
        const SizedBox(width: 12),
        _buildActionButton(
          icon: Icons.receipt_long_outlined,
          label: '充值',
          color: Colors.green,
          onTap: () => _showRecharge(context),
        ),
        const Spacer(),
        _buildActionButton(
          icon: Icons.qr_code_scanner,
          label: '扫码',
          color: Colors.orange,
          onTap: () => _showQRScan(context),
        ),
      ],
    );
  }

  Widget _buildScheduleActions(BuildContext context) {
    final now = DateTime.now();
    final hour = now.hour;

    String nextCourse = '';
    if (hour < 8) {
      nextCourse = '今日暂无课程';
    } else if (hour < 10) {
      nextCourse = '下一节:10:00 高等数学';
    } else if (hour < 12) {
      nextCourse = '下一节:14:00 英语';
    } else {
      nextCourse = '今日课程已结束';
    }

    return Row(
      children: [
        Icon(
          Icons.schedule_outlined,
          color: Colors.blue,
          size: 16,
        ),
        const SizedBox(width: 6),
        Expanded(
          child: Text(
            nextCourse,
            style: TextStyle(
              fontSize: 13,
              color: Colors.grey[700],
            ),
            maxLines: 1,
            overflow: TextOverflow.ellipsis,
          ),
        ),
      ],
    );
  }

  Widget _buildLostFoundActions(BuildContext context) {
    return Row(
      children: [
        _buildActionButton(
          icon: Icons.search_outlined,
          label: '查找',
          color: Colors.blue,
          onTap: () => _showSearch(context),
        ),
        const SizedBox(width: 12),
        _buildActionButton(
          icon: Icons.add_circle_outline,
          label: '发布',
          color: Colors.green,
          onTap: () => _showPublish(context),
        ),
      ],
    );
  }

  Widget _buildActionButton({
    required IconData icon,
    required String label,
    required Color color,
    VoidCallback? onTap,
  }) {
    return InkWell(
      onTap: onTap,
      borderRadius: BorderRadius.circular(8),
      child: Container(
        padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
        decoration: BoxDecoration(
          color: color.withOpacity(0.1),
          borderRadius: BorderRadius.circular(8),
        ),
        child: Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            Icon(
              icon,
              color: color,
              size: 16,
            ),
            const SizedBox(width: 4),
            Text(
              label,
              style: TextStyle(
                fontSize: 12,
                color: color,
                fontWeight: FontWeight.w500,
              ),
            ),
          ],
        ),
      ),
    );
  }

  void _handleTap(BuildContext context) {
    if (config.onTap != null) {
      config.onTap!();
    } else if (config.route != null) {
      // 导航到对应页面
      Navigator.pushNamed(context, config.route!);
    }
  }

  void _showBalance(BuildContext context) {
    showModalBottomSheet(
      context: context,
      builder: (context) => _BalanceBottomSheet(),
    );
  }

  void _showRecharge(BuildContext context) {
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('充值功能开发中...')),
    );
  }

  void _showQRScan(BuildContext context) {
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('扫码功能开发中...')),
    );
  }

  void _showSearch(BuildContext context) {
    Navigator.pushNamed(context, '/lost_found/search');
  }

  void _showPublish(BuildContext context) {
    Navigator.pushNamed(context, '/lost_found/publish');
  }
}

/// 余额底部弹窗
class _BalanceBottomSheet extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(24),
      decoration: const BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
      ),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          Row(
            children: [
              Container(
                width: 4,
                height: 40,
                decoration: BoxDecoration(
                  color: Colors.blue,
                  borderRadius: BorderRadius.circular(2),
                ),
              ),
              const SizedBox(width: 16),
              Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    '校园卡余额',
                    style: Theme.of(context).textTheme.titleLarge,
                  ),
                  const SizedBox(height: 4),
                  Text(
                    '卡号: **** **** **** 8888',
                    style: TextStyle(
                      fontSize: 14,
                      color: Colors.grey[600],
                    ),
                  ),
                ],
              ),
            ],
          ),
          const Divider(height: 32),
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    '当前余额',
                    style: TextStyle(
                      fontSize: 14,
                      color: Colors.grey[600],
                    ),
                  ),
                  const SizedBox(height: 8),
                  Text(
                    '¥ 256.80',
                    style: TextStyle(
                      fontSize: 32,
                      fontWeight: FontWeight.bold,
                      color: Colors.blue,
                    ),
                  ),
                ],
              ),
              Container(
                width: 80,
                height: 80,
                decoration: BoxDecoration(
                  color: Colors.grey[100],
                  borderRadius: BorderRadius.circular(8),
                ),
                child: const Center(
                  child: Icon(
                    Icons.qr_code_2,
                    size: 40,
                    color: Colors.grey,
                  ),
                ),
              ),
            ],
          ),
          const SizedBox(height: 24),
          Row(
            children: [
              Expanded(
                child: ElevatedButton.icon(
                  onPressed: () {
                    Navigator.pop(context);
                    _showTransactionHistory(context);
                  },
                  icon: const Icon(Icons.receipt_long),
                  label: '消费记录',
                  style: ElevatedButton.styleFrom(
                    backgroundColor: Colors.blue,
                    foregroundColor: Colors.white,
                    padding: const EdgeInsets.symmetric(vertical: 16),
                  ),
                ),
              ),
              const SizedBox(width: 12),
              Expanded(
                child: OutlinedButton.icon(
                  onPressed: () => Navigator.pop(context),
                  icon: const Icon(Icons.refresh),
                  label: '刷新余额',
                  style: OutlinedButton.styleFrom(
                    foregroundColor: Colors.blue,
                    padding: const EdgeInsets.symmetric(vertical: 16),
                  ),
                ),
              ),
            ],
          ),
        ],
      ),
    );
  }

  void _showTransactionHistory(BuildContext context) {
    Navigator.pushNamed(context, '/campus_card/transactions');
  }
}

4.2 卡片管理Provider

dart 复制代码
// lib/presentation/providers/service_card_provider.dart

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

import '../../core/widgets/app_card.dart';

part 'service_card_provider.g.dart';

/// 默认服务卡片配置
final _defaultServiceCards = [
  ServiceCardConfig(
    id: 'schedule',
    title: '课程表',
    description: '查看本周课程安排,设置上课提醒',
    icon: Icons.calendar_month_outlined,
    iconColor: Color(0xFF1890FF),
    route: '/schedule',
    isPinned: true,
  ),
  ServiceCardConfig(
    id: 'campus_card',
    title: '校园卡',
    description: '查询余额、消费记录,支持NFC充值',
    icon: Icons.account_balance_wallet_outlined,
    iconColor: Color(0xFF52C41A),
    route: '/campus_card',
    isPinned: true,
  ),
  ServiceCardConfig(
    id: 'library',
    title: '图书馆',
    description: '图书借阅、座位预约、NFC门禁',
    icon: Icons.local_library_outlined,
    iconColor: Color(0xFF722ED1),
    route: '/library',
  ),
  ServiceCardConfig(
    id: 'lost_found',
    title: '失物招领',
    description: '发布和查找失物,地图定位标记',
    icon: Icons.search_outlined,
    iconColor: Color(0xFFFFA940),
    route: '/lost_found',
  ),
  ServiceCardConfig(
    id: 'nfc_simulator',
    title: 'NFC模拟',
    description: '模拟NFC刷卡,测试门禁功能',
    icon: Icons.nfc_outlined,
    iconColor: Color(0xFF13C2C2),
    route: '/nfc_simulator',
  ),
  ServiceCardConfig(
    id: 'map',
    title: '校园地图',
    description: '校园导航、失物位置标注',
    icon: Icons.map_outlined,
    iconColor: Color(0xFF2ED573),
    route: '/map',
  ),
  ServiceCardConfig(
    id: 'announcements',
    title: '通知公告',
    description: '校园重要通知、活动信息推送',
    icon: Icons.notifications_outlined,
    iconColor: Color(0xFFFA541C),
    badgeCount: 3,
    isPinned: true,
  ),
  ServiceCardConfig(
    id: 'feedback',
    title: '意见反馈',
    description: '提交问题和建议,获取响应',
    icon: Icons.feedback_outlined,
    iconColor: Color(0xFF595959),
  ),
];

/// 服务卡片状态
@riverpod
class ServiceCardNotifier extends _$ServiceCardNotifier {
  @override
  List<ServiceCardConfig> build() {
    return List.from(_defaultServiceCards);
  }

  /// 切换固定状态
  void togglePin(String id) {
    state = [
      for (final card in state)
        if (card.id == id)
          card.copyWith(isPinned: !card.isPinned)
        else
          card,
    ];
  }

  /// 更新徽章数量
  void updateBadgeCount(String id, int count) {
    state = [
      for (final card in state)
        if (card.id == id)
          card.copyWith(badgeCount: count)
        else
          card,
    ];
  }

  /// 获取固定的卡片
  List<ServiceCardConfig> get pinnedCards {
    return state.where((card) => card.isPinned).toList()
      ..sort((a, b) => a.title.compareTo(b.title));
  }

  /// 获取未固定的卡片
  List<ServiceCardConfig> get unpinnedCards {
    return state.where((card) => !card.isPinned).toList()
      ..sort((a, b) => a.title.compareTo(b.title));
  }
}

五、核心页面实现

5.1 首页(服务卡片聚合)

dart 复制代码
// lib/presentation/pages/home/home_page.dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import '../../../core/widgets/app_card.dart';
import '../../../presentation/providers/service_card_provider.dart';
import '../../widgets/common/app_loading.dart';

class HomePage extends ConsumerStatefulWidget {
  const HomePage({super.key});

  @override
  ConsumerState<HomePage> createState() => _HomePageState();
}

class _HomePageState extends ConsumerState<HomePage> {
  @override
  Widget build(BuildContext context) {
    final cards = ref.watch(serviceCardNotifierProvider);
    final pinnedCards = ref.read(serviceCardNotifierProvider.notifier).pinnedCards;
    final unpinnedCards = ref.read(serviceCardNotifierProvider.notifier).unpinnedCards;

    return Scaffold(
      body: CustomScrollView(
        slivers: [
          // AppBar
          SliverAppBar(
            expandedHeight: 120,
            floating: true,
            pinned: true,
            flexibleSpace: FlexibleSpaceBar(
              title: const Text('校园通'),
              background: Container(
                decoration: BoxDecoration(
                  gradient: LinearGradient(
                    begin: Alignment.topLeft,
                    end: Alignment.bottomRight,
                    colors: [
                      Color(0xFF1890FF),
                      Color(0xFF52C41A),
                    ],
                  ),
                ),
              ),
            ),
            actions: [
              IconButton(
                icon: const Icon(Icons.settings_outlined),
                onPressed: () => _showSettings(context),
              ),
              IconButton(
                icon: const Icon(Icons.notifications_outlined),
                onPressed: () => _showNotifications(context),
              ),
            ],
          ),

          // 固定的卡片
          if (pinnedCards.isNotEmpty)
            SliverToBoxAdapter(
              child: _buildPinnedCardsSection(pinnedCards),
            ),

          // 未固定的卡片
          if (unpinnedCards.isNotEmpty)
            SliverPadding(
              padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
              sliver: SliverGrid(
                gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
                  crossAxisCount: 2,
                  mainAxisSpacing: 12,
                  crossAxisSpacing: 12,
                  childAspectRatio: 1.1,
                ),
                delegate: SliverChildBuilderDelegate(
                  childCount: unpinnedCards.length,
                  builder: (context, index) {
                    final card = unpinnedCards[index];
                    return ServiceCard(
                      config: card,
                      onPinToggle: () {
                        ref.read(serviceCardNotifierProvider.notifier).togglePin(card.id);
                      },
                    );
                  },
                ),
              ),
            ),

          // 底部间距
          const SliverToBoxAdapter(
            child: SizedBox(height: 100),
          ),
        ],
      ),
    );
  }

  Widget _buildPinnedCardsSection(List<ServiceCardConfig> pinnedCards) {
    return Container(
      margin: const EdgeInsets.fromLTRB(16, 16, 16, 0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          // 标题
          Padding(
            padding: const EdgeInsets.fromLTRB(0, 0, 0, 12),
            child: Row(
              children: [
                Icon(
                  Icons.push_pin,
                  size: 16,
                  color: Colors.blue,
                ),
                const SizedBox(width: 6),
                Text(
                  '常用服务',
                  style: TextStyle(
                    fontSize: 14,
                    fontWeight: FontWeight.w600,
                    color: Colors.grey[700],
                  ),
                ),
              ],
            ),
          ),
          // 卡片列表
          SizedBox(
            height: 160,
            child: ListView.builder(
              scrollDirection: Axis.horizontal,
              itemCount: pinnedCards.length,
              itemBuilder: (context, index) {
                final card = pinnedCards[index];
                return Container(
                  width: 280,
                  margin: const EdgeInsets.only(right: 12),
                  child: ServiceCard(config: card),
                );
              },
            ),
          ),
        ],
      ),
    );
  }

  void _showSettings(BuildContext context) {
    Navigator.pushNamed(context, '/settings');
  }

  void _showNotifications(BuildContext context) {
    Navigator.pushNamed(context, '/notifications');
  }
}

5.2 课表查询页面

dart 复制代码
// lib/presentation/pages/schedule/schedule_page.dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart';
import 'package:table_calendar/table_calendar.dart';

import '../../../data/models/course.dart';
import '../../../presentation/providers/course_provider.dart';

class SchedulePage extends ConsumerStatefulWidget {
  const SchedulePage({super.key});

  @override
  ConsumerState<SchedulePage> createState() => _SchedulePageState();
}

class _SchedulePageState extends ConsumerState<SchedulePage> {
  @override
  Widget build(BuildContext context) {
    final coursesAsync = ref.watch(courseListProvider);
    final selectedDay = ref.watch(selectedDayProvider);

    return Scaffold(
      appBar: AppBar(
        title: const Text('课程表'),
        actions: [
          IconButton(
            icon: const Icon(Icons.today_outlined),
            onPressed: () => _goToToday(),
          ),
          IconButton(
            icon: const Icon(Icons.notifications_outlined),
            onPressed: () => _showReminderSettings(context),
          ),
        ],
      ),
      body: Column(
        children: [
          // 周历选择器
          _buildWeekCalendar(),

          const Divider(height: 1),

          // 课程列表
          Expanded(
            child: coursesAsync.when(
              data: (courses) => _buildCourseList(courses, selectedDay),
              loading: () => const AppLoading(),
              error: (error, stack) => _buildErrorView(error),
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildWeekCalendar() {
    final selectedDay = ref.watch(selectedDayProvider);

    return TableCalendar(
      firstDay: DateTime.monday,
      locale: 'zh_CN',
      focusedDay: selectedDay,
      onDaySelected: (day, focusedDay) {
        ref.read(selectedDayProvider.notifier).state = day;
      },
      calendarStyle: CalendarStyle(
        outsideDaysVisible: false,
        weekendTextStyle: const TextStyle(color: Colors.red),
        selectedDecoration: BoxDecoration(
          color: Colors.blue[400],
          shape: BoxShape.circle,
        ),
        todayDecoration: BoxDecoration(
          color: Colors.blue[100],
          shape: BoxShape.circle,
        ),
      ),
      headerStyle: const HeaderStyle(
        formatButtonVisible: false,
        titleCentered: true,
      ),
    );
  }

  Widget _buildCourseList(List<Course> courses, DateTime selectedDay) {
    // 筛选选中日期的课程
    final selectedCourses = courses.where((course) {
      return course.date.year == selectedDay.year &&
          course.date.month == selectedDay.month &&
          course.date.day == selectedDay.day;
    }).toList();

    // 按时间排序
    selectedCourses.sort((a, b) => a.startTime.compareTo(b.startTime));

    if (selectedCourses.isEmpty) {
      return _buildEmptyView(selectedDay);
    }

    return ListView.builder(
      padding: const EdgeInsets.all(16),
      itemCount: selectedCourses.length,
      itemBuilder: (context, index) {
        final course = selectedCourses[index];
        return _buildCourseCard(course);
      },
    );
  }

  Widget _buildCourseCard(Course course) {
    final startTime = DateFormat('HH:mm').format(course.startTime);
    final endTime = DateFormat('HH:mm').format(course.endTime);

    return Container(
      margin: const EdgeInsets.only(bottom: 16),
      padding: const EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(16),
        boxShadow: [
          BoxShadow(
            color: Colors.black.withOpacity(0.05),
            blurRadius: 10,
            offset: const Offset(0, 2),
          ),
        ],
      ),
      child: Row(
        children: [
          // 时间列
          Container(
            width: 60,
            padding: const EdgeInsets.symmetric(vertical: 8),
            decoration: BoxDecoration(
              color: _getCourseColor(course.name).withOpacity(0.1),
              borderRadius: BorderRadius.circular(8),
            ),
            child: Column(
              children: [
                Text(
                  startTime,
                  style: TextStyle(
                    fontSize: 16,
                    fontWeight: FontWeight.bold,
                    color: _getCourseColor(course.name),
                  ),
                ),
                const SizedBox(height: 4),
                Text(
                  endTime,
                  style: TextStyle(
                    fontSize: 12,
                    color: _getCourseColor(course.name),
                  ),
                ),
              ],
            ),
          ),
          const SizedBox(width: 16),

          // 课程信息列
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  course.name,
                  style: const TextStyle(
                    fontSize: 16,
                    fontWeight: FontWeight.w600,
                  ),
                ),
                const SizedBox(height: 4),
                Text(
                  '${course.teacher} · ${course.location}',
                  style: TextStyle(
                    fontSize: 14,
                    color: Colors.grey[600],
                  ),
                ),
                if (course.note != null && course.note!.isNotEmpty) ...[
                  const SizedBox(height: 8),
                  Container(
                    padding: const EdgeInsets.all(8),
                    decoration: BoxDecoration(
                      color: Colors.orange[50],
                      borderRadius: BorderRadius.circular(8),
                    ),
                    child: Text(
                      course.note!,
                      style: TextStyle(
                        fontSize: 12,
                        color: Colors.orange[900],
                      ),
                    ),
                  ),
                ],
              ],
            ),
          ),

          // 操作按钮
          IconButton(
            icon: const Icon(Icons.notifications_outlined),
            onPressed: () => _setCourseReminder(course),
          ),
        ],
      ),
    );
  }

  Widget _buildEmptyView(DateTime selectedDay) {
    final weekday = DateFormat('EEEE', 'zh_CN').format(selectedDay);

    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Icon(
            Icons.event_available,
            size: 64,
            color: Colors.grey[400],
          ),
          const SizedBox(height: 16),
          Text(
            '$weekday 暂无课程',
            style: TextStyle(
              fontSize: 18,
              color: Colors.grey[600],
            ),
          ),
          const SizedBox(height: 8),
          Text(
            '好好享受课余时光吧!',
            style: TextStyle(
              fontSize: 14,
              color: Colors.grey[500],
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildErrorView(Object error) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          const Icon(
            Icons.error_outline,
            size: 64,
            color: Colors.red,
          ),
          const SizedBox(height: 16),
          Text(
            '加载失败',
            style: TextStyle(
              fontSize: 18,
              color: Colors.grey[700],
            ),
          ),
          const SizedBox(height: 16),
          ElevatedButton(
            onPressed: () => ref.invalidate(courseListProvider),
            child: const Text('重试'),
          ),
        ],
      ),
    );
  }

  Color _getCourseColor(String courseName) {
    // 根据课程名称生成一致的颜色
    final hash = courseName.hashCode;
    final hue = hash.abs() % 360;
    return HSVColor.fromAHSV(1, hue / 360, 0.7, 1).toColor();
  }

  void _goToToday() {
    ref.read(selectedDayProvider.notifier).state = DateTime.now();
  }

  void _showReminderSettings(BuildContext context) {
    showModalBottomSheet(
      context: context,
      builder: (context) => _ReminderSettingsSheet(),
    );
  }

  void _setCourseReminder(Course course) {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Text('已设置 ${course.name} 上课前10分钟提醒'),
        action: SnackBarAction(
          label: '查看',
          onPressed: () {},
        ),
      ),
    );
  }
}

/// 提醒设置底部弹窗
class _ReminderSettingsSheet extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(24),
      decoration: const BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
      ),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              const Text(
                '课程提醒设置',
                style: TextStyle(
                  fontSize: 18,
                  fontWeight: FontWeight.bold,
                ),
              ),
              IconButton(
                icon: const Icon(Icons.close),
                onPressed: () => Navigator.pop(context),
              ),
            ],
          ),
          const Divider(height: 24),
          SwitchListTile(
            title: const Text('上课提醒'),
            subtitle: const Text('每节课前10分钟提醒'),
            value: true,
            onChanged: (value) {},
          ),
          SwitchListTile(
            title: const Text('静音提醒'),
            subtitle: const Text('在静音模式下也提醒'),
            value: false,
            onChanged: (value) {},
          ),
          ListTile(
            title: const Text('提醒时间'),
            subtitle: const Text('提前 10 分钟'),
            leading: const Icon(Icons.schedule),
            onTap: () {
              _showReminderTimePicker(context);
            },
          ),
        ],
      ),
    );
  }

  void _showReminderTimePicker(BuildContext context) {
    showTimePicker(
      context,
      initialTime: const TimeOfDay(hour: 7, minute: 50),
    ).then((time) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('提醒时间已更新为 ${time.format(const HourMinuteFormat())}')),
      );
    });
  }
}

六、应用市场上架合规要点

6.1 module.json5权限配置

json5 复制代码
// ohos/entry/src/main/module.json5
{
  "module": {
    "name": "entry",
    "type": "entry",
    "description": "$string:module_desc",
    "mainElement": "EntryAbility",
    "deviceTypes": [
      "default",
      "tablet"
    ],
    "deliveryWithInstall": true,
    "installationFree": false,
    "pages": "$profile:main_pages",
    "abilities": [
      {
        "name": "EntryAbility",
        "srcEntry": "./ets/entryability/EntryAbility.ets",
        "description": "$string:EntryAbility_desc",
        "icon": "$media:app_icon",
        "label": "$string:EntryAbility_label",
        "startWindowIcon": "$media:app_icon",
        "startWindowBackground": "$color:start_window_background",
        "exported": true,
        "skills": [
          {
            "entities": ["entity.system.home"],
            "actions": ["action.system.home"]
          }
        ]
      },
      {
        "name": "CardServiceAbility",
        "srcEntry": "./ets/ability/CardServiceAbility.ets",
        "description": "$string:CardServiceAbility_desc",
        "icon": "$media:card_icon",
        "label": "$string:CardServiceAbility_label",
        "type": "service",
        "backgroundModes": [
          "dataTransfer",
          "location"
        ]
      }
    ],
    "requestPermissions": [
      {
        "name": "ohos.permission.INTERNET",
        "reason": "$string:internet_reason",
        "usedScene": {
          "abilities": [
            "EntryAbility",
            "CardServiceAbility"
          ],
          "when": "always"
        }
      },
      {
        "name": "ohos.permission.GET_NETWORK_INFO",
        "reason": "$string:network_info_reason"
      },
      {
        "name": "ohos.permission.LOCATION",
        "reason": "$string:location_reason",
        "usedScene": {
          "abilities": [
            "EntryAbility"
          ],
          "when": "inuse"
        }
      },
      {
        "name": "ohos.permission.APPROXIMATE_LOCATION",
        "reason": "$string:approximate_location_reason"
      },
      {
        "name": "ohos.permission.CAMERA",
        "reason": "$string:camera_reason",
        "usedScene": {
          "abilities": [
            "EntryAbility"
          ],
          "when": "inuse"
        }
      },
      {
        "name": "ohos.permission.READ_MEDIA",
        "reason": "$string:read_media_reason",
        "usedScene": {
          "abilities": [
            "EntryAbility"
          ],
          "when": "inuse"
        }
      },
      {
        "name": "ohos.permission.NOTIFICATION_CONTROLLER",
        "reason": "$string:notification_reason"
      },
      {
        "name": "ohos.permission.NFC_CONTROL",
        "reason": "$string:nfc_reason"
      },
      {
        "name": "ohos.permission.MICROPHONE",
        "reason": "$string:microphone_reason",
        "usedScene": {
          "abilities": [
            "EntryAbility"
          ],
          "when": "inuse"
        }
      }
    ]
  }
}

6.2 build-profile.json5构建配置

json5 复制代码
{
  "app": {
    "bundleName": "com.campus.pass",
    "vendor": "campus",
    "versionCode": 1000100,
    "versionName": "1.0.1",
    "icon": "$media:app_icon",
    "label": "$string:app_name",
    "description": "$string:app_description",
    "minAPIVersion": 9,
    "targetAPIVersion": 11,
    "apiReleaseType": "Release"
  },
  "modules": [
    {
      "name": "entry",
      "srcPath": "./entry/src/main",
      "targets": [
        {
          "name": "default",
          "applyToProducts": ["default"]
        }
      ]
    }
  ]
}

6.3 strings.json资源文件

json5 复制代码
// ohos/entry/src/main/resources/base/element/string.json
{
  "string": [
    {
      "name": "app_name",
      "value": "校园通"
    },
    {
      "name": "app_description",
      "value": "一站式校园服务平台,提供课表查询、校园卡、失物招领等功能"
    },
    {
      "name": "module_desc",
      "value": "校园通主模块"
    },
    {
      "name": "EntryAbility_desc",
      "value": "校园通应用主入口"
    },
    {
      "name": "EntryAbility_label",
      "value": "校园通"
    },
    {
      "name": "CardServiceAbility_desc",
      "value": "校园卡服务,支持后台NFC读取和数据同步"
    },
    {
      "name": "CardServiceAbility_label",
      "value": "校园卡服务"
    },
    {
      "name": "internet_reason",
      "value": "需要网络权限以获取课程信息、校园卡余额等数据"
    },
    {
      "name": "network_info_reason",
      "value": "需要网络状态信息以提供更好的服务体验"
    },
    {
      "name": "location_reason",
      "value": "需要位置权限以提供地图导航、失物位置标记等功能"
    },
    {
      "name": "approximate_location_reason",
      "value": "需要大概位置以显示您周围的失物信息"
    },
    {
      "name": "camera_reason",
      "value": "需要相机权限以扫描失物上的二维码、拍摄失物照片"
    },
    {
      "name": "read_media_reason",
      "value": "需要读取媒体权限以选择失物照片上传"
    },
    {
      "name": "notification_reason",
      "value": "需要推送权限以接收课程提醒、校园通知等信息"
    },
    {
      "name": "nfc_reason",
      "value": "需要NFC权限以读取校园卡、图书馆借阅证等"
    },
    {
      "name": "microphone_reason",
      "value": "需要麦克风权限以进行语音搜索、语音留言"
    }
  ]
}

6.4 AppGallery上架必备材料

材料名称 要求说明 示例
应用图标 512x512 PNG,无圆角 app_icon.png
应用截图 至少3张,最多5张,分辨率1080x2340/2340x1080 screenshot_1.png
应用描述 简洁描述70字内,详细描述2000字内 一站式校园服务平台...
应用分类 选择正确的应用分类 教育/校园生活
关键词 最多10个,逗号分隔 校园通,课程表,校园卡,失物招领,课表查询
隐私政策 必需提供隐私政策链接 https://campus.edu.cn/privacy
用户协议 必需提供用户协议链接 https://campus.edu.cn/agreement
软件著作权 需提供著作权证明 软著登字第123456号
版本说明 详细描述版本更新内容 1. 修复已知问题 2. 优化用户体验

七、项目部署与发布

7.1 打包构建命令

bash 复制代码
# 查看可用设备
flutter devices

# 构建鸿蒙HAP包(调试版本)
flutter build ohos --debug

# 构建鸿蒙HAP包(发布版本)
flutter build ohos --release

# 构建产物位置
# build/ohos/outputs/hap/release/

7.2 签名配置

json5 复制代码
// build-profile.json5 签名配置
{
  "app": {
    "signingConfigs": [
      {
        "name": "default",
        "type": "HarmonyOS",
        "material": {
          "certpath": "./campus_pass.cer",
          "storePassword": "your_store_password",
          "keyAlias": "campus_pass",
          "keyPassword": "your_key_password",
          "profile": "./campus_pass.p7b",
          "signAlg": "SHA256withECDSA",
          "verify": true,
          "verifyDetails": []
        }
      }
    ]
  }
}

7.3 发布流程

复制代码
1. 准备阶段
   ├─ 生成应用签名证书
   ├─ 准备应用图标和截图
   ├─ 编写应用描述和版本说明
   ├─ 准备隐私政策和用户协议
   └─ 申请软件著作权

2. 提交审核
   ├─ 登录华为开发者联盟
   ├─ 创建应用并填写基本信息
   ├─ 上传HAP安装包
   ├─ 提交审核材料
   └─ 等待审核结果

3. 审核通过
   ├─ 设置应用上线时间
   ├─ 配置应用推广
   └─ 监控用户反馈

4. 上线后维护
   ├─ 监控应用崩溃
   ├─ 收集用户反馈
   ├─ 及时修复Bug
   └─ 定期更新功能

参考资源


相关推荐
lqj_本人2 小时前
Flutter三方库适配OpenHarmony【apple_product_name】lookup查询方法使用技巧
flutter
lqj_本人2 小时前
Flutter三方库适配OpenHarmony【apple_product_name】设备型号标识符转换原理
运维·服务器·flutter
lqj_本人2 小时前
Flutter三方库适配OpenHarmony【apple_product_name】getMachineId方法深度解析
flutter
2401_892000523 小时前
Flutter for OpenHarmony 猫咪管家App实战:急救指南功能开发
flutter
钛态3 小时前
Flutter for OpenHarmony 实战:Pretty Dio Logger — 网络请求监控利器
flutter·microsoft·ui·华为·架构·harmonyos
2301_796512523 小时前
【精通篇】打造React Native鸿蒙跨平台开发高级复合组件库开发系列:Grid 宫格(展示内容或进行页面导航)
javascript·react native·react.js·ecmascript·harmonyos
lqj_本人3 小时前
Flutter三方库适配OpenHarmony【apple_product_name】OhosProductName类使用详解
flutter
lqj_本人3 小时前
Flutter三方库适配OpenHarmony【apple_product_name】异步调用与错误处理
flutter
木斯佳3 小时前
HarmonyOS实战(解决方案篇)—企业AI资产利旧:如何将已有智能体快速接入鸿蒙生态
人工智能·华为·harmonyos