diff --git a/mobile-app/.gitignore b/mobile-app/.gitignore
new file mode 100644
index 00000000..7b917607
--- /dev/null
+++ b/mobile-app/.gitignore
@@ -0,0 +1,43 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.buildlog/
+.history
+.svn/
+migrate_working_dir/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# Flutter/Dart/Pub related
+**/doc/api/
+**/ios/Flutter/.last_build_id
+.dart_tool/
+.flutter-plugins
+.flutter-plugins-dependencies
+.packages
+.pub-cache/
+.pub/
+/build/
+
+# Symbolication related
+app.*.symbols
+
+# Obfuscation related
+app.*.map.json
+
+# Android Studio will place build artifacts here
+/android/app/debug
+/android/app/profile
+/android/app/release
+
+# Generated files
+*.g.dart
+*.freezed.dart
diff --git a/mobile-app/README.md b/mobile-app/README.md
new file mode 100644
index 00000000..8a32d960
--- /dev/null
+++ b/mobile-app/README.md
@@ -0,0 +1,90 @@
+# 供水管理系统 - 移动端APP
+
+三合一移动应用:供水管理 / 巡检管理 / 营收管理
+
+## 技术栈
+
+- **框架**: Flutter 3.x
+- **状态管理**: Provider
+- **路由**: GoRouter
+- **HTTP**: Dio
+- **本地存储**: Hive + SharedPreferences
+- **定位**: Geolocator
+- **相机**: Image Picker
+- **通知**: Flutter Local Notifications
+
+## 项目结构
+
+```
+mobile-app/
+├── lib/
+│ ├── main.dart # 入口文件
+│ ├── core/ # 核心模块
+│ │ ├── theme/ # 主题配置
+│ │ ├── constants/ # 常量定义
+│ │ ├── network/ # 网络层(Dio + 拦截器)
+│ │ └── utils/ # 工具类
+│ ├── features/ # 功能模块
+│ │ ├── auth/ # 认证(登录/Token)
+│ │ ├── main_shell/ # 主框架(Tab导航)
+│ │ ├── water_supply/ # 供水管理
+│ │ ├── patrol/ # 巡检管理
+│ │ └── revenue/ # 营收管理
+│ ├── shared/ # 共享模块
+│ │ ├── widgets/ # 通用组件
+│ │ └── services/ # 共享服务
+│ └── config/ # 配置(路由等)
+├── android/ # Android 配置
+├── ios/ # iOS 配置
+└── pubspec.yaml # 依赖配置
+```
+
+## 功能模块
+
+### 1. 统一登录 + Token 管理
+- 账号密码登录
+- Token 自动附加(AuthInterceptor)
+- Token 刷新机制
+- 登录状态持久化
+
+### 2. 三合一 Tab 导航
+- 供水管理 Tab
+- 巡检管理 Tab
+- 营收管理 Tab
+
+### 3. 供水管理
+- 监测站点列表
+- 实时水压/流量/水质数据
+- 状态告警
+
+### 4. 巡检管理
+- 巡检任务列表(按状态筛选)
+- 任务进度展示
+- 支持新建巡检任务
+
+### 5. 营收管理
+- 抄表任务列表
+- 账单列表(缴费状态筛选)
+- 抄表数据录入
+
+### 6. 核心服务
+- **PushService**: 消息推送(初始化 + 回调)
+- **LocationService**: GPS定位 + 权限管理
+- **CameraService**: 拍照/相册选择
+- **CacheService**: 离线缓存(Hive)
+
+## 运行
+
+```bash
+# 安装依赖
+flutter pub get
+
+# 运行开发模式
+flutter run
+
+# 构建 APK
+flutter build apk
+
+# 构建 iOS
+flutter build ios
+```
diff --git a/mobile-app/analysis_options.yaml b/mobile-app/analysis_options.yaml
new file mode 100644
index 00000000..8d615798
--- /dev/null
+++ b/mobile-app/analysis_options.yaml
@@ -0,0 +1,7 @@
+include: package:flutter_lints/flutter.yaml
+
+linter:
+ rules:
+ prefer_const_constructors: true
+ prefer_const_declarations: true
+ avoid_print: false
diff --git a/mobile-app/android/app/build.gradle b/mobile-app/android/app/build.gradle
new file mode 100644
index 00000000..e06af766
--- /dev/null
+++ b/mobile-app/android/app/build.gradle
@@ -0,0 +1,65 @@
+plugins {
+ id "com.android.application"
+ id "kotlin-android"
+ id "dev.flutter.flutter-gradle-plugin"
+}
+
+def localProperties = new Properties()
+def localPropertiesFile = rootProject.file('local.properties')
+if (localPropertiesFile.exists()) {
+ localPropertiesFile.withReader('UTF-8') { reader ->
+ localProperties.load(reader)
+ }
+}
+
+def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
+if (flutterVersionCode == null) {
+ flutterVersionCode = '1'
+}
+
+def flutterVersionName = localProperties.getProperty('flutter.versionName')
+if (flutterVersionName == null) {
+ flutterVersionName = '1.0'
+}
+
+android {
+ namespace "com.xayunmei.water_management"
+ compileSdk 34
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
+ kotlinOptions {
+ jvmTarget = '1.8'
+ }
+
+ sourceSets {
+ main.java.srcDirs += 'src/main/kotlin'
+ }
+
+ defaultConfig {
+ applicationId "com.xayunmei.water_management"
+ minSdk 23
+ targetSdk 34
+ versionCode flutterVersionCode.toInteger()
+ versionName flutterVersionName
+ }
+
+ buildTypes {
+ release {
+ signingConfig signingConfigs.debug
+ minifyEnabled false
+ shrinkResources false
+ }
+ }
+}
+
+flutter {
+ source '../..'
+}
+
+dependencies {
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.10"
+}
diff --git a/mobile-app/android/app/src/main/AndroidManifest.xml b/mobile-app/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..66569e37
--- /dev/null
+++ b/mobile-app/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile-app/android/app/src/main/java/com/example/water_management/MainActivity.kt b/mobile-app/android/app/src/main/java/com/example/water_management/MainActivity.kt
new file mode 100644
index 00000000..49160d61
--- /dev/null
+++ b/mobile-app/android/app/src/main/java/com/example/water_management/MainActivity.kt
@@ -0,0 +1,6 @@
+package com.xayunmei.water_management
+
+import io.flutter.embedding.android.FlutterActivity
+
+class MainActivity: FlutterActivity() {
+}
diff --git a/mobile-app/android/app/src/main/res/values/styles.xml b/mobile-app/android/app/src/main/res/values/styles.xml
new file mode 100644
index 00000000..ff81bae8
--- /dev/null
+++ b/mobile-app/android/app/src/main/res/values/styles.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/mobile-app/android/build.gradle b/mobile-app/android/build.gradle
new file mode 100644
index 00000000..bc157bd1
--- /dev/null
+++ b/mobile-app/android/build.gradle
@@ -0,0 +1,18 @@
+allprojects {
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+rootProject.buildDir = '../build'
+subprojects {
+ project.buildDir = "${rootProject.buildDir}/${project.name}"
+}
+subprojects {
+ project.evaluationDependsOn(':app')
+}
+
+tasks.register("clean", Delete) {
+ delete rootProject.buildDir
+}
diff --git a/mobile-app/android/settings.gradle b/mobile-app/android/settings.gradle
new file mode 100644
index 00000000..e0ddd691
--- /dev/null
+++ b/mobile-app/android/settings.gradle
@@ -0,0 +1,26 @@
+pluginManagement {
+ def flutterSdkPath = {
+ def properties = new Properties()
+ file("local.properties").withInputStream { properties.load(it) }
+ def flutterSdkPath = properties.getProperty("flutter.sdk")
+ assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
+ return flutterSdkPath
+ }
+ settings.ext.flutterSdkPath = flutterSdkPath()
+
+ includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle")
+
+ repositories {
+ google()
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+
+plugins {
+ id "dev.flutter.flutter-plugin-loader" version "1.0.0"
+ id "com.android.application" version "8.1.0" apply false
+ id "org.jetbrains.kotlin.android" version "1.9.10" apply false
+}
+
+include ":app"
diff --git a/mobile-app/assets/images/.gitkeep b/mobile-app/assets/images/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/mobile-app/ios/Runner/AppDelegate.swift b/mobile-app/ios/Runner/AppDelegate.swift
new file mode 100644
index 00000000..70693e4a
--- /dev/null
+++ b/mobile-app/ios/Runner/AppDelegate.swift
@@ -0,0 +1,13 @@
+import UIKit
+import Flutter
+
+@UIApplicationMain
+@objc class AppDelegate: FlutterAppDelegate {
+ override func application(
+ _ application: UIApplication,
+ didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
+ ) -> Bool {
+ GeneratedPluginRegistrant.register(with: self)
+ return super.application(application, didFinishLaunchingWithOptions: launchOptions)
+ }
+}
diff --git a/mobile-app/ios/Runner/Info.plist b/mobile-app/ios/Runner/Info.plist
new file mode 100644
index 00000000..26825653
--- /dev/null
+++ b/mobile-app/ios/Runner/Info.plist
@@ -0,0 +1,60 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleDisplayName
+ 供水管理
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ water_management_app
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ $(FLUTTER_BUILD_NAME)
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ $(FLUTTER_BUILD_NUMBER)
+ LSRequiresIPhoneOS
+
+ UILaunchStoryboardName
+ LaunchScreen
+ UIMainStoryboardFile
+ Main
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+
+ UIViewControllerBasedStatusBarAppearance
+
+
+
+ NSLocationWhenInUseUsageDescription
+ 需要获取您的位置信息以支持巡检定位功能
+ NSLocationAlwaysUsageDescription
+ 需要持续获取您的位置信息以支持巡检轨迹记录
+
+
+ NSCameraUsageDescription
+ 需要使用相机进行巡检拍照记录
+
+
+ NSPhotoLibraryUsageDescription
+ 需要访问相册以选择巡检相关图片
+ NSPhotoLibraryAddUsageDescription
+ 需要保存巡检照片到相册
+
+
+ UIBackgroundModes
+
+ remote-notification
+
+
+
diff --git a/mobile-app/lib/config/app_config.dart b/mobile-app/lib/config/app_config.dart
new file mode 100644
index 00000000..b155464f
--- /dev/null
+++ b/mobile-app/lib/config/app_config.dart
@@ -0,0 +1,39 @@
+/// 应用全局配置
+class AppConfig {
+ static AppConfig? _instance;
+
+ AppConfig._internal();
+
+ factory AppConfig() {
+ _instance ??= AppConfig._internal();
+ return _instance!;
+ }
+
+ /// 环境配置
+ AppEnvironment _environment = AppEnvironment.development;
+ AppEnvironment get environment => _environment;
+
+ /// 切换环境
+ void setEnvironment(AppEnvironment env) {
+ _environment = env;
+ }
+
+ /// API 基础地址
+ String get baseUrl {
+ switch (_environment) {
+ case AppEnvironment.development:
+ return 'https://dev-api.xayunmei.com';
+ case AppEnvironment.staging:
+ return 'https://staging-api.xayunmei.com';
+ case AppEnvironment.production:
+ return 'https://api.xayunmei.com';
+ }
+ }
+}
+
+/// 环境枚举
+enum AppEnvironment {
+ development,
+ staging,
+ production,
+}
diff --git a/mobile-app/lib/config/app_routes.dart b/mobile-app/lib/config/app_routes.dart
new file mode 100644
index 00000000..201ae464
--- /dev/null
+++ b/mobile-app/lib/config/app_routes.dart
@@ -0,0 +1,42 @@
+import 'package:flutter/material.dart';
+import 'package:go_router/go_router.dart';
+import '../features/auth/pages/login_page.dart';
+import '../features/auth/services/token_service.dart';
+import '../features/main_shell/pages/main_shell_page.dart';
+import '../features/revenue/pages/bill_list_page.dart';
+
+/// 应用路由配置
+class AppRoutes {
+ static const String login = '/login';
+ static const String main = '/main';
+ static const String bills = '/bills';
+
+ static GoRouter createRouter() {
+ return GoRouter(
+ initialLocation: login,
+ redirect: (BuildContext context, GoRouterState state) async {
+ final tokenService = TokenService();
+ final isLoggedIn = await tokenService.isLoggedIn();
+ final isGoingToLogin = state.matchedLocation == login;
+
+ if (!isLoggedIn && !isGoingToLogin) return login;
+ if (isLoggedIn && isGoingToLogin) return main;
+ return null;
+ },
+ routes: [
+ GoRoute(
+ path: login,
+ builder: (context, state) => const LoginPage(),
+ ),
+ GoRoute(
+ path: main,
+ builder: (context, state) => const MainShellPage(),
+ ),
+ GoRoute(
+ path: bills,
+ builder: (context, state) => const BillListPage(),
+ ),
+ ],
+ );
+ }
+}
diff --git a/mobile-app/lib/core/constants/app_constants.dart b/mobile-app/lib/core/constants/app_constants.dart
new file mode 100644
index 00000000..582d7190
--- /dev/null
+++ b/mobile-app/lib/core/constants/app_constants.dart
@@ -0,0 +1,25 @@
+/// 应用常量配置
+class AppConstants {
+ // API 基础地址
+ static const String baseUrl = 'https://api.xayunmei.com';
+ static const String apiVersion = '/api/v1';
+
+ // Token 相关
+ static const String tokenKey = 'auth_token';
+ static const String refreshTokenKey = 'refresh_token';
+ static const String tokenExpiryKey = 'token_expiry';
+
+ // 缓存相关
+ static const String cacheBoxName = 'app_cache';
+ static const String userBoxName = 'user_cache';
+
+ // 超时配置
+ static const int connectTimeout = 15000;
+ static const int receiveTimeout = 15000;
+
+ // 分页配置
+ static const int defaultPageSize = 20;
+
+ // 应用名称
+ static const String appName = '供水管理系统';
+}
diff --git a/mobile-app/lib/core/network/api_response.dart b/mobile-app/lib/core/network/api_response.dart
new file mode 100644
index 00000000..be46839b
--- /dev/null
+++ b/mobile-app/lib/core/network/api_response.dart
@@ -0,0 +1,39 @@
+/// 统一 API 响应模型
+class ApiResponse {
+ final int code;
+ final String message;
+ final T? data;
+
+ ApiResponse({
+ required this.code,
+ required this.message,
+ this.data,
+ });
+
+ bool get isSuccess => code == 200 || code == 0;
+
+ factory ApiResponse.fromJson(Map json, T Function(dynamic)? fromJsonT) {
+ return ApiResponse(
+ code: json['code'] ?? 0,
+ message: json['message'] ?? '',
+ data: json['data'] != null && fromJsonT != null ? fromJsonT(json['data']) : json['data'] as T?,
+ );
+ }
+}
+
+/// 分页响应模型
+class PaginatedResponse {
+ final List list;
+ final int total;
+ final int page;
+ final int pageSize;
+
+ PaginatedResponse({
+ required this.list,
+ required this.total,
+ required this.page,
+ required this.pageSize,
+ });
+
+ bool get hasMore => page * pageSize < total;
+}
diff --git a/mobile-app/lib/core/network/auth_interceptor.dart b/mobile-app/lib/core/network/auth_interceptor.dart
new file mode 100644
index 00000000..a8a59387
--- /dev/null
+++ b/mobile-app/lib/core/network/auth_interceptor.dart
@@ -0,0 +1,41 @@
+import 'package:dio/dio.dart';
+import '../../features/auth/services/token_service.dart';
+import '../constants/app_constants.dart';
+
+/// Token 认证拦截器
+/// 自动在请求头中附加 Token,处理 Token 刷新
+class AuthInterceptor extends Interceptor {
+ final TokenService _tokenService = TokenService();
+
+ @override
+ void onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
+ final token = await _tokenService.getToken();
+ if (token != null) {
+ options.headers['Authorization'] = 'Bearer $token';
+ }
+ handler.next(options);
+ }
+
+ @override
+ void onError(DioException err, ErrorInterceptorHandler handler) async {
+ if (err.response?.statusCode == 401) {
+ // Token 过期,尝试刷新
+ try {
+ final refreshed = await _tokenService.refreshToken();
+ if (refreshed) {
+ // 使用新 Token 重试请求
+ final token = await _tokenService.getToken();
+ err.requestOptions.headers['Authorization'] = 'Bearer $token';
+
+ final response = await Dio().fetch(err.requestOptions);
+ handler.resolve(response);
+ return;
+ }
+ } catch (_) {
+ // 刷新失败,清除 Token 并跳转登录
+ await _tokenService.clearTokens();
+ }
+ }
+ handler.next(err);
+ }
+}
diff --git a/mobile-app/lib/core/network/dio_client.dart b/mobile-app/lib/core/network/dio_client.dart
new file mode 100644
index 00000000..09004a7a
--- /dev/null
+++ b/mobile-app/lib/core/network/dio_client.dart
@@ -0,0 +1,79 @@
+import 'package:dio/dio.dart';
+import '../constants/app_constants.dart';
+import 'auth_interceptor.dart';
+
+/// Dio HTTP 客户端配置
+class DioClient {
+ static DioClient? _instance;
+ late Dio _dio;
+
+ Dio get dio => _dio;
+
+ DioClient._internal() {
+ _dio = Dio(
+ BaseOptions(
+ baseUrl: AppConstants.baseUrl + AppConstants.apiVersion,
+ connectTimeout: const Duration(milliseconds: AppConstants.connectTimeout),
+ receiveTimeout: const Duration(milliseconds: AppConstants.receiveTimeout),
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json',
+ },
+ ),
+ );
+
+ // 添加拦截器
+ _dio.interceptors.addAll([
+ AuthInterceptor(),
+ LogInterceptor(
+ requestBody: true,
+ responseBody: true,
+ logPrint: (obj) => print('[DioLog] $obj'),
+ ),
+ ]);
+ }
+
+ factory DioClient() {
+ _instance ??= DioClient._internal();
+ return _instance!;
+ }
+
+ /// GET 请求
+ Future> get(
+ String path, {
+ Map? queryParameters,
+ Options? options,
+ }) async {
+ return _dio.get(path, queryParameters: queryParameters, options: options);
+ }
+
+ /// POST 请求
+ Future> post(
+ String path, {
+ dynamic data,
+ Map? queryParameters,
+ Options? options,
+ }) async {
+ return _dio.post(path, data: data, queryParameters: queryParameters, options: options);
+ }
+
+ /// PUT 请求
+ Future> put(
+ String path, {
+ dynamic data,
+ Map? queryParameters,
+ Options? options,
+ }) async {
+ return _dio.put(path, data: data, queryParameters: queryParameters, options: options);
+ }
+
+ /// DELETE 请求
+ Future> delete(
+ String path, {
+ dynamic data,
+ Map? queryParameters,
+ Options? options,
+ }) async {
+ return _dio.delete(path, data: data, queryParameters: queryParameters, options: options);
+ }
+}
diff --git a/mobile-app/lib/core/theme/app_theme.dart b/mobile-app/lib/core/theme/app_theme.dart
new file mode 100644
index 00000000..f8f6f216
--- /dev/null
+++ b/mobile-app/lib/core/theme/app_theme.dart
@@ -0,0 +1,52 @@
+import 'package:flutter/material.dart';
+
+/// 应用主题配置
+class AppTheme {
+ static const Color primaryColor = Color(0xFF1976D2);
+ static const Color secondaryColor = Color(0xFF42A5F5);
+ static const Color accentColor = Color(0xFF00BCD4);
+ static const Color backgroundColor = Color(0xFFF5F5F5);
+ static const Color errorColor = Color(0xFFD32F2F);
+ static const Color successColor = Color(0xFF388E3C);
+
+ static ThemeData get lightTheme {
+ return ThemeData(
+ useMaterial3: true,
+ primaryColor: primaryColor,
+ colorScheme: ColorScheme.fromSeed(
+ seedColor: primaryColor,
+ secondary: secondaryColor,
+ background: backgroundColor,
+ error: errorColor,
+ ),
+ appBarTheme: const AppBarTheme(
+ backgroundColor: primaryColor,
+ foregroundColor: Colors.white,
+ elevation: 0,
+ ),
+ elevatedButtonTheme: ElevatedButtonThemeData(
+ style: ElevatedButton.styleFrom(
+ backgroundColor: primaryColor,
+ foregroundColor: Colors.white,
+ minimumSize: const Size(double.infinity, 48),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(8),
+ ),
+ ),
+ ),
+ inputDecorationTheme: InputDecorationTheme(
+ border: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(8),
+ ),
+ filled: true,
+ fillColor: Colors.white,
+ ),
+ cardTheme: CardTheme(
+ elevation: 2,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(12),
+ ),
+ ),
+ );
+ }
+}
diff --git a/mobile-app/lib/features/auth/models/user_model.dart b/mobile-app/lib/features/auth/models/user_model.dart
new file mode 100644
index 00000000..f680e75f
--- /dev/null
+++ b/mobile-app/lib/features/auth/models/user_model.dart
@@ -0,0 +1,44 @@
+/// 用户模型
+class UserModel {
+ final String id;
+ final String username;
+ final String name;
+ final String? avatar;
+ final String role;
+ final String? department;
+ final String? phone;
+
+ UserModel({
+ required this.id,
+ required this.username,
+ required this.name,
+ this.avatar,
+ required this.role,
+ this.department,
+ this.phone,
+ });
+
+ factory UserModel.fromJson(Map json) {
+ return UserModel(
+ id: json['id']?.toString() ?? '',
+ username: json['username'] ?? '',
+ name: json['name'] ?? '',
+ avatar: json['avatar'],
+ role: json['role'] ?? '',
+ department: json['department'],
+ phone: json['phone'],
+ );
+ }
+
+ Map toJson() {
+ return {
+ 'id': id,
+ 'username': username,
+ 'name': name,
+ 'avatar': avatar,
+ 'role': role,
+ 'department': department,
+ 'phone': phone,
+ };
+ }
+}
diff --git a/mobile-app/lib/features/auth/pages/login_page.dart b/mobile-app/lib/features/auth/pages/login_page.dart
new file mode 100644
index 00000000..3b5c159e
--- /dev/null
+++ b/mobile-app/lib/features/auth/pages/login_page.dart
@@ -0,0 +1,173 @@
+import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+import '../services/auth_provider.dart';
+
+/// 登录页面
+class LoginPage extends StatefulWidget {
+ const LoginPage({super.key});
+
+ @override
+ State createState() => _LoginPageState();
+}
+
+class _LoginPageState extends State {
+ final _formKey = GlobalKey();
+ final _usernameController = TextEditingController();
+ final _passwordController = TextEditingController();
+ bool _obscurePassword = true;
+
+ @override
+ void dispose() {
+ _usernameController.dispose();
+ _passwordController.dispose();
+ super.dispose();
+ }
+
+ Future _handleLogin() async {
+ if (!_formKey.currentState!.validate()) return;
+
+ final authProvider = Provider.of(context, listen: false);
+ final success = await authProvider.login(
+ username: _usernameController.text.trim(),
+ password: _passwordController.text,
+ );
+
+ if (success && mounted) {
+ // 登录成功后路由由 AuthGuard 自动处理
+ Navigator.of(context).pushReplacementNamed('/main');
+ } else if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text(authProvider.errorMessage ?? '登录失败'),
+ backgroundColor: Colors.red,
+ ),
+ );
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ body: SafeArea(
+ child: Center(
+ child: SingleChildScrollView(
+ padding: const EdgeInsets.all(32),
+ child: Form(
+ key: _formKey,
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ // Logo 和应用名
+ const Icon(
+ Icons.water_drop,
+ size: 80,
+ color: Color(0xFF1976D2),
+ ),
+ const SizedBox(height: 16),
+ const Text(
+ '供水管理系统',
+ textAlign: TextAlign.center,
+ style: TextStyle(
+ fontSize: 24,
+ fontWeight: FontWeight.bold,
+ color: Color(0xFF1976D2),
+ ),
+ ),
+ const SizedBox(height: 8),
+ Text(
+ '供水 · 巡检 · 营收',
+ textAlign: TextAlign.center,
+ style: TextStyle(
+ fontSize: 14,
+ color: Colors.grey[600],
+ ),
+ ),
+ const SizedBox(height: 48),
+
+ // 用户名输入
+ TextFormField(
+ controller: _usernameController,
+ decoration: const InputDecoration(
+ labelText: '用户名',
+ prefixIcon: Icon(Icons.person),
+ hintText: '请输入用户名',
+ ),
+ validator: (value) {
+ if (value == null || value.trim().isEmpty) {
+ return '请输入用户名';
+ }
+ return null;
+ },
+ ),
+ const SizedBox(height: 16),
+
+ // 密码输入
+ TextFormField(
+ controller: _passwordController,
+ decoration: InputDecoration(
+ labelText: '密码',
+ prefixIcon: const Icon(Icons.lock),
+ hintText: '请输入密码',
+ suffixIcon: IconButton(
+ icon: Icon(
+ _obscurePassword ? Icons.visibility_off : Icons.visibility,
+ ),
+ onPressed: () {
+ setState(() {
+ _obscurePassword = !_obscurePassword;
+ });
+ },
+ ),
+ ),
+ obscureText: _obscurePassword,
+ validator: (value) {
+ if (value == null || value.isEmpty) {
+ return '请输入密码';
+ }
+ if (value.length < 6) {
+ return '密码长度不能少于6位';
+ }
+ return null;
+ },
+ ),
+ const SizedBox(height: 32),
+
+ // 登录按钮
+ Consumer(
+ builder: (context, auth, child) {
+ return ElevatedButton(
+ onPressed: auth.isLoading ? null : _handleLogin,
+ child: auth.isLoading
+ ? const SizedBox(
+ height: 20,
+ width: 20,
+ child: CircularProgressIndicator(
+ strokeWidth: 2,
+ color: Colors.white,
+ ),
+ )
+ : const Text(
+ '登 录',
+ style: TextStyle(fontSize: 16),
+ ),
+ );
+ },
+ ),
+ const SizedBox(height: 16),
+
+ // 版本信息
+ Text(
+ 'v1.0.0',
+ textAlign: TextAlign.center,
+ style: TextStyle(color: Colors.grey[400], fontSize: 12),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/mobile-app/lib/features/auth/services/auth_provider.dart b/mobile-app/lib/features/auth/services/auth_provider.dart
new file mode 100644
index 00000000..f95e98eb
--- /dev/null
+++ b/mobile-app/lib/features/auth/services/auth_provider.dart
@@ -0,0 +1,82 @@
+import 'package:flutter/material.dart';
+import 'token_service.dart';
+import '../models/user_model.dart';
+
+/// 认证状态管理
+class AuthProvider extends ChangeNotifier {
+ final TokenService _tokenService = TokenService();
+
+ bool _isLoading = false;
+ bool _isAuthenticated = false;
+ UserModel? _currentUser;
+ String? _errorMessage;
+
+ bool get isLoading => _isLoading;
+ bool get isAuthenticated => _isAuthenticated;
+ UserModel? get currentUser => _currentUser;
+ String? get errorMessage => _errorMessage;
+
+ /// 初始化认证状态
+ Future init() async {
+ _isAuthenticated = await _tokenService.isLoggedIn();
+ if (_isAuthenticated) {
+ // TODO: 从接口获取用户信息
+ _currentUser = UserModel(
+ id: '1',
+ username: 'admin',
+ name: '管理员',
+ role: 'admin',
+ );
+ }
+ notifyListeners();
+ }
+
+ /// 登录
+ Future login({
+ required String username,
+ required String password,
+ }) async {
+ _isLoading = true;
+ _errorMessage = null;
+ notifyListeners();
+
+ try {
+ // TODO: 调用实际登录接口
+ // final response = await DioClient().post('/auth/login', data: {
+ // 'username': username,
+ // 'password': password,
+ // });
+
+ // 模拟登录成功
+ await _tokenService.saveTokens(
+ token: 'mock_access_token_${DateTime.now().millisecondsSinceEpoch}',
+ refreshToken: 'mock_refresh_token',
+ expiresIn: 7200,
+ );
+
+ _currentUser = UserModel(
+ id: '1',
+ username: username,
+ name: username == 'admin' ? '管理员' : username,
+ role: 'admin',
+ );
+ _isAuthenticated = true;
+ _isLoading = false;
+ notifyListeners();
+ return true;
+ } catch (e) {
+ _errorMessage = '登录失败,请检查用户名和密码';
+ _isLoading = false;
+ notifyListeners();
+ return false;
+ }
+ }
+
+ /// 登出
+ Future logout() async {
+ await _tokenService.clearTokens();
+ _isAuthenticated = false;
+ _currentUser = null;
+ notifyListeners();
+ }
+}
diff --git a/mobile-app/lib/features/auth/services/token_service.dart b/mobile-app/lib/features/auth/services/token_service.dart
new file mode 100644
index 00000000..131fda7b
--- /dev/null
+++ b/mobile-app/lib/features/auth/services/token_service.dart
@@ -0,0 +1,90 @@
+import 'package:shared_preferences/shared_preferences.dart';
+import '../../../core/constants/app_constants.dart';
+
+/// Token 管理服务
+/// 负责 Token 的存储、读取、刷新和清除
+class TokenService {
+ static TokenService? _instance;
+ SharedPreferences? _prefs;
+
+ TokenService();
+
+ factory TokenService.getInstance() {
+ _instance ??= TokenService();
+ return _instance!;
+ }
+
+ Future get _preferences async {
+ _prefs ??= await SharedPreferences.getInstance();
+ return _prefs!;
+ }
+
+ /// 获取 Access Token
+ Future getToken() async {
+ final prefs = await _preferences;
+ return prefs.getString(AppConstants.tokenKey);
+ }
+
+ /// 获取 Refresh Token
+ Future getRefreshToken() async {
+ final prefs = await _preferences;
+ return prefs.getString(AppConstants.refreshTokenKey);
+ }
+
+ /// 保存 Token
+ Future saveTokens({
+ required String token,
+ String? refreshToken,
+ int? expiresIn,
+ }) async {
+ final prefs = await _preferences;
+ await prefs.setString(AppConstants.tokenKey, token);
+ if (refreshToken != null) {
+ await prefs.setString(AppConstants.refreshTokenKey, refreshToken);
+ }
+ if (expiresIn != null) {
+ final expiry = DateTime.now().millisecondsSinceEpoch + expiresIn * 1000;
+ await prefs.setInt(AppConstants.tokenExpiryKey, expiry);
+ }
+ }
+
+ /// 刷新 Token
+ Future refreshToken() async {
+ // TODO: 实现实际的 Token 刷新逻辑
+ // 使用 refreshToken 调用后端接口获取新的 accessToken
+ final refreshTok = await getRefreshToken();
+ if (refreshTok == null) return false;
+
+ try {
+ // 模拟刷新
+ // final response = await DioClient().post('/auth/refresh', data: {'refresh_token': refreshTok});
+ // await saveTokens(token: response.data['data']['token'], refreshToken: response.data['data']['refresh_token']);
+ return true;
+ } catch (e) {
+ return false;
+ }
+ }
+
+ /// 检查 Token 是否过期
+ Future isTokenExpired() async {
+ final prefs = await _preferences;
+ final expiry = prefs.getInt(AppConstants.tokenExpiryKey);
+ if (expiry == null) return true;
+ return DateTime.now().millisecondsSinceEpoch > expiry;
+ }
+
+ /// 清除所有 Token
+ Future clearTokens() async {
+ final prefs = await _preferences;
+ await prefs.remove(AppConstants.tokenKey);
+ await prefs.remove(AppConstants.refreshTokenKey);
+ await prefs.remove(AppConstants.tokenExpiryKey);
+ }
+
+ /// 是否已登录
+ Future isLoggedIn() async {
+ final token = await getToken();
+ if (token == null) return false;
+ return !(await isTokenExpired());
+ }
+}
diff --git a/mobile-app/lib/features/main_shell/pages/main_shell_page.dart b/mobile-app/lib/features/main_shell/pages/main_shell_page.dart
new file mode 100644
index 00000000..67ee6870
--- /dev/null
+++ b/mobile-app/lib/features/main_shell/pages/main_shell_page.dart
@@ -0,0 +1,57 @@
+import 'package:flutter/material.dart';
+import '../../water_supply/pages/monitor_list_page.dart';
+import '../../patrol/pages/patrol_task_list_page.dart';
+import '../../revenue/pages/meter_reading_page.dart';
+
+/// 主页面 - 底部三Tab导航(供水/巡检/营收)
+class MainShellPage extends StatefulWidget {
+ const MainShellPage({super.key});
+
+ @override
+ State createState() => _MainShellPageState();
+}
+
+class _MainShellPageState extends State {
+ int _currentIndex = 0;
+
+ final List _pages = const [
+ MonitorListPage(),
+ PatrolTaskListPage(),
+ MeterReadingPage(),
+ ];
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ body: IndexedStack(
+ index: _currentIndex,
+ children: _pages,
+ ),
+ bottomNavigationBar: BottomNavigationBar(
+ currentIndex: _currentIndex,
+ onTap: (index) {
+ setState(() {
+ _currentIndex = index;
+ });
+ },
+ type: BottomNavigationBarType.fixed,
+ selectedItemColor: const Color(0xFF1976D2),
+ unselectedItemColor: Colors.grey,
+ items: const [
+ BottomNavigationBarItem(
+ icon: Icon(Icons.water_drop),
+ label: '供水管理',
+ ),
+ BottomNavigationBarItem(
+ icon: Icon(Icons.assignment),
+ label: '巡检',
+ ),
+ BottomNavigationBarItem(
+ icon: Icon(Icons.receipt_long),
+ label: '营收',
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/mobile-app/lib/features/patrol/models/patrol_task_model.dart b/mobile-app/lib/features/patrol/models/patrol_task_model.dart
new file mode 100644
index 00000000..87912bd5
--- /dev/null
+++ b/mobile-app/lib/features/patrol/models/patrol_task_model.dart
@@ -0,0 +1,59 @@
+/// 巡检任务模型
+class PatrolTaskModel {
+ final String id;
+ final String taskName;
+ final String taskCode;
+ final String routeName;
+ final String assignee;
+ final String status; // pending, in_progress, completed, overdue
+ final DateTime planDate;
+ final int checkpointTotal;
+ final int checkpointCompleted;
+ final String? remark;
+
+ PatrolTaskModel({
+ required this.id,
+ required this.taskName,
+ required this.taskCode,
+ required this.routeName,
+ required this.assignee,
+ required this.status,
+ required this.planDate,
+ required this.checkpointTotal,
+ required this.checkpointCompleted,
+ this.remark,
+ });
+
+ double get progress =>
+ checkpointTotal > 0 ? checkpointCompleted / checkpointTotal : 0;
+
+ factory PatrolTaskModel.fromJson(Map json) {
+ return PatrolTaskModel(
+ id: json['id']?.toString() ?? '',
+ taskName: json['taskName'] ?? '',
+ taskCode: json['taskCode'] ?? '',
+ routeName: json['routeName'] ?? '',
+ assignee: json['assignee'] ?? '',
+ status: json['status'] ?? 'pending',
+ planDate: DateTime.tryParse(json['planDate'] ?? '') ?? DateTime.now(),
+ checkpointTotal: json['checkpointTotal'] ?? 0,
+ checkpointCompleted: json['checkpointCompleted'] ?? 0,
+ remark: json['remark'],
+ );
+ }
+
+ Map toJson() {
+ return {
+ 'id': id,
+ 'taskName': taskName,
+ 'taskCode': taskCode,
+ 'routeName': routeName,
+ 'assignee': assignee,
+ 'status': status,
+ 'planDate': planDate.toIso8601String(),
+ 'checkpointTotal': checkpointTotal,
+ 'checkpointCompleted': checkpointCompleted,
+ 'remark': remark,
+ };
+ }
+}
diff --git a/mobile-app/lib/features/patrol/pages/patrol_task_list_page.dart b/mobile-app/lib/features/patrol/pages/patrol_task_list_page.dart
new file mode 100644
index 00000000..0c016e43
--- /dev/null
+++ b/mobile-app/lib/features/patrol/pages/patrol_task_list_page.dart
@@ -0,0 +1,255 @@
+import 'package:flutter/material.dart';
+import '../models/patrol_task_model.dart';
+
+/// 巡检任务列表页面
+class PatrolTaskListPage extends StatefulWidget {
+ const PatrolTaskListPage({super.key});
+
+ @override
+ State createState() => _PatrolTaskListPageState();
+}
+
+class _PatrolTaskListPageState extends State
+ with AutomaticKeepAliveClientMixin {
+ List _tasks = [];
+ bool _isLoading = true;
+ String _filterStatus = 'all';
+
+ @override
+ bool get wantKeepAlive => true;
+
+ @override
+ void initState() {
+ super.initState();
+ _loadData();
+ }
+
+ void _loadData() {
+ Future.delayed(const Duration(milliseconds: 500), () {
+ setState(() {
+ _tasks = _generateMockData();
+ _isLoading = false;
+ });
+ });
+ }
+
+ List _generateMockData() {
+ return [
+ PatrolTaskModel(
+ id: '1', taskName: '城东管网日常巡检', taskCode: 'PT-2024-001',
+ routeName: '城东A线', assignee: '张工',
+ status: 'in_progress', planDate: DateTime.now(),
+ checkpointTotal: 12, checkpointCompleted: 7,
+ ),
+ PatrolTaskModel(
+ id: '2', taskName: '城西水厂设备巡检', taskCode: 'PT-2024-002',
+ routeName: '城西厂区', assignee: '李工',
+ status: 'pending', planDate: DateTime.now().add(const Duration(days: 1)),
+ checkpointTotal: 8, checkpointCompleted: 0,
+ ),
+ PatrolTaskModel(
+ id: '3', taskName: '南区管网夜间巡检', taskCode: 'PT-2024-003',
+ routeName: '南区B线', assignee: '王工',
+ status: 'completed', planDate: DateTime.now().subtract(const Duration(days: 1)),
+ checkpointTotal: 10, checkpointCompleted: 10,
+ ),
+ PatrolTaskModel(
+ id: '4', taskName: '北区调蓄池安全巡检', taskCode: 'PT-2024-004',
+ routeName: '北区站点', assignee: '赵工',
+ status: 'overdue', planDate: DateTime.now().subtract(const Duration(days: 2)),
+ checkpointTotal: 6, checkpointCompleted: 2,
+ ),
+ PatrolTaskModel(
+ id: '5', taskName: '开发区阀门巡检', taskCode: 'PT-2024-005',
+ routeName: '开发区C线', assignee: '刘工',
+ status: 'pending', planDate: DateTime.now().add(const Duration(days: 2)),
+ checkpointTotal: 15, checkpointCompleted: 0,
+ ),
+ ];
+ }
+
+ List get _filteredTasks {
+ if (_filterStatus == 'all') return _tasks;
+ return _tasks.where((t) => t.status == _filterStatus).toList();
+ }
+
+ Color _getStatusColor(String status) {
+ switch (status) {
+ case 'pending': return Colors.blue;
+ case 'in_progress': return Colors.orange;
+ case 'completed': return Colors.green;
+ case 'overdue': return Colors.red;
+ default: return Colors.grey;
+ }
+ }
+
+ String _getStatusText(String status) {
+ switch (status) {
+ case 'pending': return '待执行';
+ case 'in_progress': return '进行中';
+ case 'completed': return '已完成';
+ case 'overdue': return '已超期';
+ default: return status;
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ super.build(context);
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('巡检任务'),
+ bottom: PreferredSize(
+ preferredSize: const Size.fromHeight(48),
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 12),
+ child: Row(
+ children: [
+ _buildFilterChip('全部', 'all'),
+ _buildFilterChip('待执行', 'pending'),
+ _buildFilterChip('进行中', 'in_progress'),
+ _buildFilterChip('已完成', 'completed'),
+ _buildFilterChip('超期', 'overdue'),
+ ],
+ ),
+ ),
+ ),
+ ),
+ body: _isLoading
+ ? const Center(child: CircularProgressIndicator())
+ : _filteredTasks.isEmpty
+ ? const Center(child: Text('暂无巡检任务'))
+ : ListView.builder(
+ padding: const EdgeInsets.all(12),
+ itemCount: _filteredTasks.length,
+ itemBuilder: (context, index) {
+ return _buildTaskCard(_filteredTasks[index]);
+ },
+ ),
+ floatingActionButton: FloatingActionButton(
+ onPressed: () {
+ // TODO: 新建巡检任务
+ },
+ child: const Icon(Icons.add),
+ ),
+ );
+ }
+
+ Widget _buildFilterChip(String label, String value) {
+ final isSelected = _filterStatus == value;
+ return Padding(
+ padding: const EdgeInsets.only(right: 8),
+ child: FilterChip(
+ label: Text(label, style: const TextStyle(fontSize: 12)),
+ selected: isSelected,
+ onSelected: (selected) {
+ setState(() {
+ _filterStatus = value;
+ });
+ },
+ selectedColor: const Color(0xFF1976D2).withOpacity(0.2),
+ checkmarkColor: const Color(0xFF1976D2),
+ ),
+ );
+ }
+
+ Widget _buildTaskCard(PatrolTaskModel task) {
+ return Card(
+ margin: const EdgeInsets.only(bottom: 12),
+ child: Padding(
+ padding: const EdgeInsets.all(16),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ const Icon(Icons.assignment, color: Color(0xFF1976D2), size: 20),
+ const SizedBox(width: 8),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ task.taskName,
+ style: const TextStyle(
+ fontSize: 15,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ Text(
+ task.taskCode,
+ style: TextStyle(fontSize: 12, color: Colors.grey[500]),
+ ),
+ ],
+ ),
+ ),
+ Container(
+ padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
+ decoration: BoxDecoration(
+ color: _getStatusColor(task.status).withOpacity(0.1),
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: Text(
+ _getStatusText(task.status),
+ style: TextStyle(
+ fontSize: 12,
+ color: _getStatusColor(task.status),
+ ),
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ Row(
+ children: [
+ _buildInfoItem(Icons.route, '路线: ${task.routeName}'),
+ const SizedBox(width: 16),
+ _buildInfoItem(Icons.person, '巡检人: ${task.assignee}'),
+ ],
+ ),
+ const SizedBox(height: 8),
+ // 进度条
+ Row(
+ children: [
+ Text(
+ '巡检进度: ${task.checkpointCompleted}/${task.checkpointTotal}',
+ style: const TextStyle(fontSize: 12),
+ ),
+ const Spacer(),
+ Text(
+ '${(task.progress * 100).toStringAsFixed(0)}%',
+ style: TextStyle(
+ fontSize: 12,
+ color: _getStatusColor(task.status),
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 4),
+ ClipRRect(
+ borderRadius: BorderRadius.circular(4),
+ child: LinearProgressIndicator(
+ value: task.progress,
+ backgroundColor: Colors.grey[200],
+ valueColor: AlwaysStoppedAnimation(_getStatusColor(task.status)),
+ minHeight: 6,
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildInfoItem(IconData icon, String text) {
+ return Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Icon(icon, size: 14, color: Colors.grey[600]),
+ const SizedBox(width: 4),
+ Text(text, style: TextStyle(fontSize: 13, color: Colors.grey[700])),
+ ],
+ );
+ }
+}
diff --git a/mobile-app/lib/features/revenue/models/bill_model.dart b/mobile-app/lib/features/revenue/models/bill_model.dart
new file mode 100644
index 00000000..666ab161
--- /dev/null
+++ b/mobile-app/lib/features/revenue/models/bill_model.dart
@@ -0,0 +1,64 @@
+/// 账单模型
+class BillModel {
+ final String id;
+ final String billNo;
+ final String userName;
+ final String userAddress;
+ final String meterNo;
+ final double previousReading;
+ final double currentReading;
+ final double usage;
+ final double amount;
+ final String status; // unpaid, paid, overdue
+ final String period;
+ final DateTime dueDate;
+
+ BillModel({
+ required this.id,
+ required this.billNo,
+ required this.userName,
+ required this.userAddress,
+ required this.meterNo,
+ required this.previousReading,
+ required this.currentReading,
+ required this.usage,
+ required this.amount,
+ required this.status,
+ required this.period,
+ required this.dueDate,
+ });
+
+ factory BillModel.fromJson(Map json) {
+ return BillModel(
+ id: json['id']?.toString() ?? '',
+ billNo: json['billNo'] ?? '',
+ userName: json['userName'] ?? '',
+ userAddress: json['userAddress'] ?? '',
+ meterNo: json['meterNo'] ?? '',
+ previousReading: (json['previousReading'] ?? 0).toDouble(),
+ currentReading: (json['currentReading'] ?? 0).toDouble(),
+ usage: (json['usage'] ?? 0).toDouble(),
+ amount: (json['amount'] ?? 0).toDouble(),
+ status: json['status'] ?? 'unpaid',
+ period: json['period'] ?? '',
+ dueDate: DateTime.tryParse(json['dueDate'] ?? '') ?? DateTime.now(),
+ );
+ }
+
+ Map toJson() {
+ return {
+ 'id': id,
+ 'billNo': billNo,
+ 'userName': userName,
+ 'userAddress': userAddress,
+ 'meterNo': meterNo,
+ 'previousReading': previousReading,
+ 'currentReading': currentReading,
+ 'usage': usage,
+ 'amount': amount,
+ 'status': status,
+ 'period': period,
+ 'dueDate': dueDate.toIso8601String(),
+ };
+ }
+}
diff --git a/mobile-app/lib/features/revenue/models/meter_reading_model.dart b/mobile-app/lib/features/revenue/models/meter_reading_model.dart
new file mode 100644
index 00000000..118a33a9
--- /dev/null
+++ b/mobile-app/lib/features/revenue/models/meter_reading_model.dart
@@ -0,0 +1,59 @@
+/// 抄表记录模型
+class MeterReadingModel {
+ final String id;
+ final String meterNo;
+ final String userName;
+ final String address;
+ final double previousReading;
+ final double? currentReading;
+ final String status; // pending, completed, abnormal
+ final DateTime readingDate;
+ final String? reader;
+ final String? remark;
+
+ MeterReadingModel({
+ required this.id,
+ required this.meterNo,
+ required this.userName,
+ required this.address,
+ required this.previousReading,
+ this.currentReading,
+ required this.status,
+ required this.readingDate,
+ this.reader,
+ this.remark,
+ });
+
+ double? get usage =>
+ currentReading != null ? currentReading! - previousReading : null;
+
+ factory MeterReadingModel.fromJson(Map json) {
+ return MeterReadingModel(
+ id: json['id']?.toString() ?? '',
+ meterNo: json['meterNo'] ?? '',
+ userName: json['userName'] ?? '',
+ address: json['address'] ?? '',
+ previousReading: (json['previousReading'] ?? 0).toDouble(),
+ currentReading: json['currentReading']?.toDouble(),
+ status: json['status'] ?? 'pending',
+ readingDate: DateTime.tryParse(json['readingDate'] ?? '') ?? DateTime.now(),
+ reader: json['reader'],
+ remark: json['remark'],
+ );
+ }
+
+ Map toJson() {
+ return {
+ 'id': id,
+ 'meterNo': meterNo,
+ 'userName': userName,
+ 'address': address,
+ 'previousReading': previousReading,
+ 'currentReading': currentReading,
+ 'status': status,
+ 'readingDate': readingDate.toIso8601String(),
+ 'reader': reader,
+ 'remark': remark,
+ };
+ }
+}
diff --git a/mobile-app/lib/features/revenue/pages/bill_list_page.dart b/mobile-app/lib/features/revenue/pages/bill_list_page.dart
new file mode 100644
index 00000000..5beec55e
--- /dev/null
+++ b/mobile-app/lib/features/revenue/pages/bill_list_page.dart
@@ -0,0 +1,223 @@
+import 'package:flutter/material.dart';
+import '../models/bill_model.dart';
+
+/// 账单列表页面
+class BillListPage extends StatefulWidget {
+ const BillListPage({super.key});
+
+ @override
+ State createState() => _BillListPageState();
+}
+
+class _BillListPageState extends State {
+ List _bills = [];
+ bool _isLoading = true;
+ String _filterStatus = 'all';
+
+ @override
+ void initState() {
+ super.initState();
+ _loadData();
+ }
+
+ void _loadData() {
+ Future.delayed(const Duration(milliseconds: 500), () {
+ setState(() {
+ _bills = _generateMockData();
+ _isLoading = false;
+ });
+ });
+ }
+
+ List _generateMockData() {
+ return [
+ BillModel(
+ id: '1', billNo: 'BILL-2024-001', userName: '张三',
+ userAddress: '城东街道12号', meterNo: 'WM-2024-001',
+ previousReading: 1250.5, currentReading: 1285.0,
+ usage: 34.5, amount: 138.0, status: 'unpaid',
+ period: '2024-01', dueDate: DateTime.now().add(const Duration(days: 15)),
+ ),
+ BillModel(
+ id: '2', billNo: 'BILL-2024-002', userName: '李四',
+ userAddress: '城西大道88号', meterNo: 'WM-2024-002',
+ previousReading: 3420.0, currentReading: 3456.0,
+ usage: 36.0, amount: 144.0, status: 'paid',
+ period: '2024-01', dueDate: DateTime.now().subtract(const Duration(days: 5)),
+ ),
+ BillModel(
+ id: '3', billNo: 'BILL-2024-003', userName: '王五',
+ userAddress: '南区花园5栋', meterNo: 'WM-2024-003',
+ previousReading: 890.0, currentReading: 932.0,
+ usage: 42.0, amount: 168.0, status: 'overdue',
+ period: '2023-12', dueDate: DateTime.now().subtract(const Duration(days: 10)),
+ ),
+ BillModel(
+ id: '4', billNo: 'BILL-2024-004', userName: '赵六',
+ userAddress: '北区商铺16号', meterNo: 'WM-2024-004',
+ previousReading: 5600.0, currentReading: 5780.0,
+ usage: 180.0, amount: 720.0, status: 'unpaid',
+ period: '2024-01', dueDate: DateTime.now().add(const Duration(days: 20)),
+ ),
+ ];
+ }
+
+ List get _filteredBills {
+ if (_filterStatus == 'all') return _bills;
+ return _bills.where((b) => b.status == _filterStatus).toList();
+ }
+
+ Color _getStatusColor(String status) {
+ switch (status) {
+ case 'unpaid': return Colors.orange;
+ case 'paid': return Colors.green;
+ case 'overdue': return Colors.red;
+ default: return Colors.grey;
+ }
+ }
+
+ String _getStatusText(String status) {
+ switch (status) {
+ case 'unpaid': return '未缴费';
+ case 'paid': return '已缴费';
+ case 'overdue': return '已逾期';
+ default: return status;
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('账单列表'),
+ ),
+ body: _isLoading
+ ? const Center(child: CircularProgressIndicator())
+ : Column(
+ children: [
+ // 筛选
+ Padding(
+ padding: const EdgeInsets.all(12),
+ child: Row(
+ children: [
+ _buildFilterChip('全部', 'all'),
+ _buildFilterChip('未缴费', 'unpaid'),
+ _buildFilterChip('已缴费', 'paid'),
+ _buildFilterChip('已逾期', 'overdue'),
+ ],
+ ),
+ ),
+ // 列表
+ Expanded(
+ child: _filteredBills.isEmpty
+ ? const Center(child: Text('暂无账单'))
+ : ListView.builder(
+ padding: const EdgeInsets.symmetric(horizontal: 12),
+ itemCount: _filteredBills.length,
+ itemBuilder: (context, index) {
+ return _buildBillCard(_filteredBills[index]);
+ },
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildFilterChip(String label, String value) {
+ final isSelected = _filterStatus == value;
+ return Padding(
+ padding: const EdgeInsets.only(right: 8),
+ child: FilterChip(
+ label: Text(label, style: const TextStyle(fontSize: 12)),
+ selected: isSelected,
+ onSelected: (selected) {
+ setState(() {
+ _filterStatus = value;
+ });
+ },
+ selectedColor: const Color(0xFF1976D2).withOpacity(0.2),
+ checkmarkColor: const Color(0xFF1976D2),
+ ),
+ );
+ }
+
+ Widget _buildBillCard(BillModel bill) {
+ return Card(
+ margin: const EdgeInsets.only(bottom: 12),
+ child: Padding(
+ padding: const EdgeInsets.all(16),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ const Icon(Icons.receipt, color: Color(0xFF1976D2), size: 20),
+ const SizedBox(width: 8),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(bill.userName, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
+ Text(bill.billNo, style: TextStyle(fontSize: 12, color: Colors.grey[500])),
+ ],
+ ),
+ ),
+ Container(
+ padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
+ decoration: BoxDecoration(
+ color: _getStatusColor(bill.status).withOpacity(0.1),
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: Text(
+ _getStatusText(bill.status),
+ style: TextStyle(fontSize: 12, color: _getStatusColor(bill.status)),
+ ),
+ ),
+ ],
+ ),
+ const Divider(height: 20),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ _buildDetailItem('用水量', '${bill.usage.toStringAsFixed(1)} m³'),
+ _buildDetailItem('水费', '¥${bill.amount.toStringAsFixed(2)}'),
+ _buildDetailItem('账期', bill.period),
+ ],
+ ),
+ const SizedBox(height: 8),
+ Text(
+ '地址: ${bill.userAddress}',
+ style: TextStyle(fontSize: 12, color: Colors.grey[600]),
+ ),
+ if (bill.status != 'paid') ...[
+ const SizedBox(height: 12),
+ Align(
+ alignment: Alignment.centerRight,
+ child: ElevatedButton(
+ onPressed: () {
+ // TODO: 缴费操作
+ },
+ style: ElevatedButton.styleFrom(
+ minimumSize: const Size(100, 36),
+ ),
+ child: const Text('去缴费'),
+ ),
+ ),
+ ],
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildDetailItem(String label, String value) {
+ return Column(
+ children: [
+ Text(label, style: TextStyle(fontSize: 12, color: Colors.grey[600])),
+ const SizedBox(height: 4),
+ Text(value, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600)),
+ ],
+ );
+ }
+}
diff --git a/mobile-app/lib/features/revenue/pages/meter_reading_page.dart b/mobile-app/lib/features/revenue/pages/meter_reading_page.dart
new file mode 100644
index 00000000..8eb149a4
--- /dev/null
+++ b/mobile-app/lib/features/revenue/pages/meter_reading_page.dart
@@ -0,0 +1,238 @@
+import 'package:flutter/material.dart';
+import '../models/meter_reading_model.dart';
+import 'bill_list_page.dart';
+
+/// 抄表页面(营收 Tab 入口,包含抄表列表和账单入口)
+class MeterReadingPage extends StatefulWidget {
+ const MeterReadingPage({super.key});
+
+ @override
+ State createState() => _MeterReadingPageState();
+}
+
+class _MeterReadingPageState extends State
+ with AutomaticKeepAliveClientMixin {
+ List _readings = [];
+ bool _isLoading = true;
+
+ @override
+ bool get wantKeepAlive => true;
+
+ @override
+ void initState() {
+ super.initState();
+ _loadData();
+ }
+
+ void _loadData() {
+ Future.delayed(const Duration(milliseconds: 500), () {
+ setState(() {
+ _readings = _generateMockData();
+ _isLoading = false;
+ });
+ });
+ }
+
+ List _generateMockData() {
+ return [
+ MeterReadingModel(
+ id: '1', meterNo: 'WM-2024-001', userName: '张三',
+ address: '城东街道12号', previousReading: 1250.5,
+ status: 'pending', readingDate: DateTime.now(),
+ ),
+ MeterReadingModel(
+ id: '2', meterNo: 'WM-2024-002', userName: '李四',
+ address: '城西大道88号', previousReading: 3420.0,
+ currentReading: 3456.0, status: 'completed',
+ readingDate: DateTime.now().subtract(const Duration(days: 1)),
+ reader: '王工',
+ ),
+ MeterReadingModel(
+ id: '3', meterNo: 'WM-2024-003', userName: '王五',
+ address: '南区花园5栋', previousReading: 890.0,
+ status: 'pending', readingDate: DateTime.now(),
+ ),
+ MeterReadingModel(
+ id: '4', meterNo: 'WM-2024-004', userName: '赵六',
+ address: '北区商铺16号', previousReading: 5600.0,
+ currentReading: 5780.0, status: 'completed',
+ readingDate: DateTime.now().subtract(const Duration(days: 2)),
+ reader: '李工',
+ ),
+ MeterReadingModel(
+ id: '5', meterNo: 'WM-2024-005', userName: '孙七',
+ address: '中心路28号', previousReading: 2100.0,
+ status: 'abnormal', readingDate: DateTime.now(),
+ remark: '水表损坏',
+ ),
+ ];
+ }
+
+ Color _getStatusColor(String status) {
+ switch (status) {
+ case 'pending': return Colors.orange;
+ case 'completed': return Colors.green;
+ case 'abnormal': return Colors.red;
+ default: return Colors.grey;
+ }
+ }
+
+ String _getStatusText(String status) {
+ switch (status) {
+ case 'pending': return '待抄表';
+ case 'completed': return '已完成';
+ case 'abnormal': return '异常';
+ default: return status;
+ }
+ }
+
+ void _showReadingDialog(MeterReadingModel reading) {
+ final controller = TextEditingController();
+ showDialog(
+ context: context,
+ builder: (ctx) => AlertDialog(
+ title: Text('抄表 - ${reading.userName}'),
+ content: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text('表号: ${reading.meterNo}'),
+ Text('上次读数: ${reading.previousReading}'),
+ const SizedBox(height: 12),
+ TextField(
+ controller: controller,
+ keyboardType: const TextInputType.numberWithOptions(decimal: true),
+ decoration: const InputDecoration(
+ labelText: '当前读数',
+ hintText: '请输入当前水表读数',
+ ),
+ ),
+ ],
+ ),
+ actions: [
+ TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('取消')),
+ ElevatedButton(
+ onPressed: () {
+ // TODO: 提交抄表数据
+ Navigator.pop(ctx);
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(content: Text('抄表数据已提交')),
+ );
+ },
+ child: const Text('提交'),
+ ),
+ ],
+ ),
+ );
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ super.build(context);
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('营收管理'),
+ actions: [
+ IconButton(
+ icon: const Icon(Icons.receipt_long),
+ tooltip: '账单列表',
+ onPressed: () {
+ Navigator.of(context).push(
+ MaterialPageRoute(builder: (_) => const BillListPage()),
+ );
+ },
+ ),
+ ],
+ ),
+ body: _isLoading
+ ? const Center(child: CircularProgressIndicator())
+ : Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // 统计卡片
+ Padding(
+ padding: const EdgeInsets.all(12),
+ child: Row(
+ children: [
+ _buildStatCard('待抄表', '${_readings.where((r) => r.status == 'pending').length}', Colors.orange),
+ const SizedBox(width: 8),
+ _buildStatCard('已完成', '${_readings.where((r) => r.status == 'completed').length}', Colors.green),
+ const SizedBox(width: 8),
+ _buildStatCard('异常', '${_readings.where((r) => r.status == 'abnormal').length}', Colors.red),
+ ],
+ ),
+ ),
+ const Padding(
+ padding: EdgeInsets.symmetric(horizontal: 12),
+ child: Text('抄表任务', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
+ ),
+ // 抄表列表
+ Expanded(
+ child: ListView.builder(
+ padding: const EdgeInsets.all(12),
+ itemCount: _readings.length,
+ itemBuilder: (context, index) {
+ final reading = _readings[index];
+ return Card(
+ margin: const EdgeInsets.only(bottom: 8),
+ child: ListTile(
+ leading: CircleAvatar(
+ backgroundColor: _getStatusColor(reading.status).withOpacity(0.1),
+ child: Icon(
+ Icons.speed,
+ color: _getStatusColor(reading.status),
+ ),
+ ),
+ title: Text(reading.userName),
+ subtitle: Text('${reading.meterNo} · ${reading.address}'),
+ trailing: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ crossAxisAlignment: CrossAxisAlignment.end,
+ children: [
+ Text(
+ _getStatusText(reading.status),
+ style: TextStyle(
+ fontSize: 12,
+ color: _getStatusColor(reading.status),
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ if (reading.status == 'pending')
+ TextButton(
+ onPressed: () => _showReadingDialog(reading),
+ style: TextButton.styleFrom(
+ padding: EdgeInsets.zero,
+ minimumSize: const Size(0, 0),
+ tapTargetSize: MaterialTapTargetSize.shrinkWrap,
+ ),
+ child: const Text('抄表', style: TextStyle(fontSize: 12)),
+ ),
+ ],
+ ),
+ ),
+ );
+ },
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildStatCard(String label, String count, Color color) {
+ return Expanded(
+ child: Card(
+ child: Padding(
+ padding: const EdgeInsets.all(12),
+ child: Column(
+ children: [
+ Text(count, style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: color)),
+ const SizedBox(height: 4),
+ Text(label, style: TextStyle(fontSize: 12, color: Colors.grey[600])),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/mobile-app/lib/features/water_supply/models/monitor_data_model.dart b/mobile-app/lib/features/water_supply/models/monitor_data_model.dart
new file mode 100644
index 00000000..f88f06db
--- /dev/null
+++ b/mobile-app/lib/features/water_supply/models/monitor_data_model.dart
@@ -0,0 +1,48 @@
+/// 监测数据模型
+class MonitorDataModel {
+ final String id;
+ final String stationName;
+ final String stationCode;
+ final double pressure;
+ final double flow;
+ final double quality;
+ final String status;
+ final DateTime updateTime;
+
+ MonitorDataModel({
+ required this.id,
+ required this.stationName,
+ required this.stationCode,
+ required this.pressure,
+ required this.flow,
+ required this.quality,
+ required this.status,
+ required this.updateTime,
+ });
+
+ factory MonitorDataModel.fromJson(Map json) {
+ return MonitorDataModel(
+ id: json['id']?.toString() ?? '',
+ stationName: json['stationName'] ?? '',
+ stationCode: json['stationCode'] ?? '',
+ pressure: (json['pressure'] ?? 0).toDouble(),
+ flow: (json['flow'] ?? 0).toDouble(),
+ quality: (json['quality'] ?? 0).toDouble(),
+ status: json['status'] ?? 'normal',
+ updateTime: DateTime.tryParse(json['updateTime'] ?? '') ?? DateTime.now(),
+ );
+ }
+
+ Map toJson() {
+ return {
+ 'id': id,
+ 'stationName': stationName,
+ 'stationCode': stationCode,
+ 'pressure': pressure,
+ 'flow': flow,
+ 'quality': quality,
+ 'status': status,
+ 'updateTime': updateTime.toIso8601String(),
+ };
+ }
+}
diff --git a/mobile-app/lib/features/water_supply/pages/monitor_list_page.dart b/mobile-app/lib/features/water_supply/pages/monitor_list_page.dart
new file mode 100644
index 00000000..6bf7ca17
--- /dev/null
+++ b/mobile-app/lib/features/water_supply/pages/monitor_list_page.dart
@@ -0,0 +1,194 @@
+import 'package:flutter/material.dart';
+import '../models/monitor_data_model.dart';
+
+/// 供水监测数据列表页面
+class MonitorListPage extends StatefulWidget {
+ const MonitorListPage({super.key});
+
+ @override
+ State createState() => _MonitorListPageState();
+}
+
+class _MonitorListPageState extends State
+ with AutomaticKeepAliveClientMixin {
+ List _monitorData = [];
+ bool _isLoading = true;
+
+ @override
+ bool get wantKeepAlive => true;
+
+ @override
+ void initState() {
+ super.initState();
+ _loadData();
+ }
+
+ void _loadData() {
+ // 模拟数据加载
+ Future.delayed(const Duration(milliseconds: 500), () {
+ setState(() {
+ _monitorData = _generateMockData();
+ _isLoading = false;
+ });
+ });
+ }
+
+ List _generateMockData() {
+ final stations = [
+ ('城东加压站', 'ST-001'),
+ ('城西水厂', 'ST-002'),
+ ('南区配水站', 'ST-003'),
+ ('北区调蓄池', 'ST-004'),
+ ('中心泵站', 'ST-005'),
+ ('开发区监测点', 'ST-006'),
+ ('高新区水厂', 'ST-007'),
+ ('工业园加压站', 'ST-008'),
+ ];
+
+ return stations.map((station) {
+ return MonitorDataModel(
+ id: station.$2,
+ stationName: station.$1,
+ stationCode: station.$2,
+ pressure: 0.2 + (station.$2.hashCode % 30) / 100.0,
+ flow: 100 + (station.$2.hashCode % 500).toDouble(),
+ quality: 95 + (station.$2.hashCode % 5).toDouble(),
+ status: station.$2.hashCode % 7 == 0 ? 'warning' : 'normal',
+ updateTime: DateTime.now().subtract(
+ Duration(minutes: station.$2.hashCode % 60),
+ ),
+ );
+ }).toList();
+ }
+
+ Color _getStatusColor(String status) {
+ switch (status) {
+ case 'normal':
+ return Colors.green;
+ case 'warning':
+ return Colors.orange;
+ case 'error':
+ return Colors.red;
+ default:
+ return Colors.grey;
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ super.build(context);
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('供水监测'),
+ actions: [
+ IconButton(
+ icon: const Icon(Icons.refresh),
+ onPressed: () {
+ setState(() {
+ _isLoading = true;
+ });
+ _loadData();
+ },
+ ),
+ ],
+ ),
+ body: _isLoading
+ ? const Center(child: CircularProgressIndicator())
+ : RefreshIndicator(
+ onRefresh: () async {
+ setState(() {
+ _isLoading = true;
+ });
+ _loadData();
+ },
+ child: ListView.builder(
+ padding: const EdgeInsets.all(12),
+ itemCount: _monitorData.length,
+ itemBuilder: (context, index) {
+ final item = _monitorData[index];
+ return _buildMonitorCard(item);
+ },
+ ),
+ ),
+ );
+ }
+
+ Widget _buildMonitorCard(MonitorDataModel item) {
+ return Card(
+ margin: const EdgeInsets.only(bottom: 12),
+ child: Padding(
+ padding: const EdgeInsets.all(16),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ const Icon(Icons.water_drop, color: Color(0xFF1976D2), size: 20),
+ const SizedBox(width: 8),
+ Expanded(
+ child: Text(
+ item.stationName,
+ style: const TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ),
+ Container(
+ padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
+ decoration: BoxDecoration(
+ color: _getStatusColor(item.status).withOpacity(0.1),
+ borderRadius: BorderRadius.circular(12),
+ border: Border.all(
+ color: _getStatusColor(item.status),
+ ),
+ ),
+ child: Text(
+ item.status == 'normal' ? '正常' : '告警',
+ style: TextStyle(
+ fontSize: 12,
+ color: _getStatusColor(item.status),
+ ),
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ Row(
+ children: [
+ _buildDataItem('水压', '${item.pressure.toStringAsFixed(2)} MPa'),
+ _buildDataItem('流量', '${item.flow.toStringAsFixed(1)} m³/h'),
+ _buildDataItem('水质', '${item.quality.toStringAsFixed(1)}%'),
+ ],
+ ),
+ const SizedBox(height: 8),
+ Text(
+ '更新时间: ${item.updateTime.month}/${item.updateTime.day} '
+ '${item.updateTime.hour.toString().padLeft(2, '0')}:'
+ '${item.updateTime.minute.toString().padLeft(2, '0')}',
+ style: TextStyle(fontSize: 12, color: Colors.grey[500]),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildDataItem(String label, String value) {
+ return Expanded(
+ child: Column(
+ children: [
+ Text(label, style: TextStyle(fontSize: 12, color: Colors.grey[600])),
+ const SizedBox(height: 4),
+ Text(
+ value,
+ style: const TextStyle(
+ fontSize: 14,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/mobile-app/lib/main.dart b/mobile-app/lib/main.dart
new file mode 100644
index 00000000..6c533c05
--- /dev/null
+++ b/mobile-app/lib/main.dart
@@ -0,0 +1,54 @@
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:provider/provider.dart';
+import 'core/theme/app_theme.dart';
+import 'config/app_routes.dart';
+import 'features/auth/services/auth_provider.dart';
+import 'shared/services/push_service.dart';
+import 'shared/services/cache_service.dart';
+
+void main() async {
+ WidgetsFlutterBinding.ensureInitialized();
+
+ // 强制竖屏
+ await SystemChrome.setPreferredOrientations([
+ DeviceOrientation.portraitUp,
+ DeviceOrientation.portraitDown,
+ ]);
+
+ // 设置状态栏样式
+ SystemChrome.setSystemUIOverlayStyle(
+ const SystemUiOverlayStyle(
+ statusBarColor: Colors.transparent,
+ statusBarIconBrightness: Brightness.light,
+ ),
+ );
+
+ // 初始化服务
+ await PushService().initialize();
+ await CacheService().initialize();
+
+ runApp(const WaterManagementApp());
+}
+
+/// 供水管理系统三合一APP
+class WaterManagementApp extends StatelessWidget {
+ const WaterManagementApp({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return MultiProvider(
+ providers: [
+ ChangeNotifierProvider(
+ create: (_) => AuthProvider()..init(),
+ ),
+ ],
+ child: MaterialApp.router(
+ title: '供水管理系统',
+ debugShowCheckedModeBanner: false,
+ theme: AppTheme.lightTheme,
+ routerConfig: AppRoutes.createRouter(),
+ ),
+ );
+ }
+}
diff --git a/mobile-app/lib/shared/services/cache_service.dart b/mobile-app/lib/shared/services/cache_service.dart
new file mode 100644
index 00000000..90ef7eea
--- /dev/null
+++ b/mobile-app/lib/shared/services/cache_service.dart
@@ -0,0 +1,122 @@
+import 'dart:convert';
+
+/// 离线缓存服务
+/// 基于 Hive 提供本地数据存储能力
+class CacheService {
+ static CacheService? _instance;
+ final Map _memoryCache = {};
+ bool _isInitialized = false;
+
+ CacheService._internal();
+
+ factory CacheService() {
+ _instance ??= CacheService._internal();
+ return _instance!;
+ }
+
+ /// 初始化缓存(打开 Hive Box)
+ Future initialize() async {
+ if (_isInitialized) return;
+
+ try {
+ // TODO: 初始化 Hive
+ // await Hive.initFlutter();
+ // await Hive.openBox(AppConstants.cacheBoxName);
+ // await Hive.openBox(AppConstants.userBoxName);
+ _isInitialized = true;
+ print('[CacheService] 缓存服务已初始化(模拟)');
+ } catch (e) {
+ print('[CacheService] 初始化失败: $e');
+ }
+ }
+
+ /// 写入缓存
+ Future put(String key, dynamic value, {String box = 'default'}) async {
+ try {
+ // TODO: 使用 Hive 持久化
+ // final cacheBox = Hive.box(AppConstants.cacheBoxName);
+ // await cacheBox.put(key, value);
+ _memoryCache['$box:$key'] = value;
+ } catch (e) {
+ print('[CacheService] 写入缓存失败: $e');
+ }
+ }
+
+ /// 读取缓存
+ Future get(String key, {String box = 'default'}) async {
+ try {
+ // TODO: 使用 Hive 读取
+ // final cacheBox = Hive.box(AppConstants.cacheBoxName);
+ // return cacheBox.get(key) as T?;
+ return _memoryCache['$box:$key'] as T?;
+ } catch (e) {
+ print('[CacheService] 读取缓存失败: $e');
+ return null;
+ }
+ }
+
+ /// 删除缓存
+ Future delete(String key, {String box = 'default'}) async {
+ try {
+ _memoryCache.remove('$box:$key');
+ } catch (e) {
+ print('[CacheService] 删除缓存失败: $e');
+ }
+ }
+
+ /// 清空指定 Box 的所有缓存
+ Future clearBox({String box = 'default'}) async {
+ try {
+ _memoryCache.removeWhere((key, _) => key.startsWith('$box:'));
+ } catch (e) {
+ print('[CacheService] 清空缓存失败: $e');
+ }
+ }
+
+ /// 清空所有缓存
+ Future clearAll() async {
+ try {
+ _memoryCache.clear();
+ } catch (e) {
+ print('[CacheService] 清空所有缓存失败: $e');
+ }
+ }
+
+ /// 缓存 JSON 对象
+ Future cacheJson(String key, Map data, {String box = 'default'}) async {
+ await put(key, json.encode(data), box: box);
+ }
+
+ /// 读取缓存的 JSON 对象
+ Future