feat(mobile): #24 Flutter三合一APP骨架搭建
- Flutter 3.x 项目初始化(pubspec.yaml + 目录结构) - 核心依赖:dio/go_router/provider/shared_preferences/hive/geolocator - 统一登录页 + Token管理(TokenService + AuthInterceptor) - 底部Tab三合一导航(供水/巡检/营收) - 供水管理Tab:监测数据列表(MonitorListPage) - 巡检Tab:任务列表+状态筛选+进度展示(PatrolTaskListPage) - 营收Tab:抄表页面+账单列表(MeterReadingPage/BillListPage) - 消息推送服务(PushService) - GPS定位服务(LocationService) - 拍照/相册服务(CameraService) - 离线缓存服务(CacheService) - Android/iOS打包配置+权限声明 - GoRouter路由+Auth认证守卫 - Provider状态管理
This commit is contained in:
@@ -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<String, dynamic> 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<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'username': username,
|
||||
'name': name,
|
||||
'avatar': avatar,
|
||||
'role': role,
|
||||
'department': department,
|
||||
'phone': phone,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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<LoginPage> createState() => _LoginPageState();
|
||||
}
|
||||
|
||||
class _LoginPageState extends State<LoginPage> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _usernameController = TextEditingController();
|
||||
final _passwordController = TextEditingController();
|
||||
bool _obscurePassword = true;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_usernameController.dispose();
|
||||
_passwordController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _handleLogin() async {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
|
||||
final authProvider = Provider.of<AuthProvider>(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<AuthProvider>(
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<void> init() async {
|
||||
_isAuthenticated = await _tokenService.isLoggedIn();
|
||||
if (_isAuthenticated) {
|
||||
// TODO: 从接口获取用户信息
|
||||
_currentUser = UserModel(
|
||||
id: '1',
|
||||
username: 'admin',
|
||||
name: '管理员',
|
||||
role: 'admin',
|
||||
);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 登录
|
||||
Future<bool> 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<void> logout() async {
|
||||
await _tokenService.clearTokens();
|
||||
_isAuthenticated = false;
|
||||
_currentUser = null;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
@@ -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<SharedPreferences> get _preferences async {
|
||||
_prefs ??= await SharedPreferences.getInstance();
|
||||
return _prefs!;
|
||||
}
|
||||
|
||||
/// 获取 Access Token
|
||||
Future<String?> getToken() async {
|
||||
final prefs = await _preferences;
|
||||
return prefs.getString(AppConstants.tokenKey);
|
||||
}
|
||||
|
||||
/// 获取 Refresh Token
|
||||
Future<String?> getRefreshToken() async {
|
||||
final prefs = await _preferences;
|
||||
return prefs.getString(AppConstants.refreshTokenKey);
|
||||
}
|
||||
|
||||
/// 保存 Token
|
||||
Future<void> 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<bool> 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<bool> isTokenExpired() async {
|
||||
final prefs = await _preferences;
|
||||
final expiry = prefs.getInt(AppConstants.tokenExpiryKey);
|
||||
if (expiry == null) return true;
|
||||
return DateTime.now().millisecondsSinceEpoch > expiry;
|
||||
}
|
||||
|
||||
/// 清除所有 Token
|
||||
Future<void> clearTokens() async {
|
||||
final prefs = await _preferences;
|
||||
await prefs.remove(AppConstants.tokenKey);
|
||||
await prefs.remove(AppConstants.refreshTokenKey);
|
||||
await prefs.remove(AppConstants.tokenExpiryKey);
|
||||
}
|
||||
|
||||
/// 是否已登录
|
||||
Future<bool> isLoggedIn() async {
|
||||
final token = await getToken();
|
||||
if (token == null) return false;
|
||||
return !(await isTokenExpired());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user