后端: - User表加Role字段 + DoctorProfile表 + 废弃旧Doctor数据 - JWT加Role claim + 注册/登录拆分(/api/auth/register + /api/auth/login) - 医生端点全量加JWT鉴权+Role校验(15个端点) - 日历端点改造(用药/运动/随访按日期+星期匹配) - 饮食记录PUT端点 + 健康记录DELETE端点 - 用户Profile端点返回Role 前端: - 登录页重做(角色选择卡片 用户紫/医生蓝 + 审核码6666 + 注册返登录) - App路由分流(userRole→患者端/医生端) - 医生端: 工作台Dashboard + 患者管理(搜索分页) + 问诊/报告/随访CRUD - 报告审核: AI预分析+指标表格+严重程度4级+模板多选+评语 - 随访编辑: 患者选择器+日期时间+打通患者端 - 健康日历: 可点击日历+当日安排列表+用药/运动/随访颜色标记 - 饮食记录: 热量趋势7/30天+日历(一周)+当日胶囊详情+侧滑编辑删除 - 健康档案: 日期选择器+手术可添加多条+白底渐变保存按钮 - 配色系统: 清理25个未使用色+新增doctorBlue/drawerGradient/pageGrey - 患者端侧边栏: 新渐变+白底按钮+紫色汉堡图标 - AI对话: 用户气泡白底黑字+药管家改蓝色 - App启动闪屏: 防登录页闪烁 文档: color_design_system.md(20页面完整配色设计+图标底色对照表)
159 lines
5.1 KiB
Dart
159 lines
5.1 KiB
Dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:dio/dio.dart';
|
|
import '../core/api_client.dart';
|
|
import '../core/local_database.dart';
|
|
|
|
class UserInfo {
|
|
final String id;
|
|
final String phone;
|
|
final String role;
|
|
final String? name;
|
|
final String? avatarUrl;
|
|
|
|
UserInfo({required this.id, required this.phone, this.role = 'User', this.name, this.avatarUrl});
|
|
}
|
|
|
|
class AuthState {
|
|
final UserInfo? user;
|
|
final bool isLoggedIn;
|
|
final bool isLoading;
|
|
|
|
const AuthState({this.user, this.isLoggedIn = false, this.isLoading = true});
|
|
}
|
|
|
|
final authProvider = NotifierProvider<AuthNotifier, AuthState>(AuthNotifier.new);
|
|
|
|
final localDbProvider = Provider<LocalDatabase>((ref) => LocalDatabase.instance);
|
|
|
|
final apiClientProvider = Provider<ApiClient>((ref) {
|
|
return ApiClient(db: ref.watch(localDbProvider));
|
|
});
|
|
|
|
class AuthNotifier extends Notifier<AuthState> {
|
|
@override
|
|
AuthState build() {
|
|
_checkAuth();
|
|
return const AuthState(isLoading: true);
|
|
}
|
|
|
|
Future<void> _checkAuth() async {
|
|
final db = ref.read(localDbProvider);
|
|
final refresh = await db.read('refresh_token');
|
|
if (refresh == null) {
|
|
state = const AuthState(isLoggedIn: false, isLoading: false);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
final response = await Dio(BaseOptions(baseUrl: baseUrl))
|
|
.post('/api/auth/refresh', data: {'refreshToken': refresh});
|
|
final data = response.data['data'];
|
|
if (data != null) {
|
|
await db.write('access_token', data['accessToken']);
|
|
await db.write('refresh_token', data['refreshToken']);
|
|
final u = data['user'] as Map<String, dynamic>?;
|
|
state = AuthState(isLoggedIn: true, isLoading: false,
|
|
user: UserInfo(id: '', phone: '', role: u?['role'] ?? 'User'),
|
|
);
|
|
_loadProfile();
|
|
} else {
|
|
state = const AuthState(isLoggedIn: false, isLoading: false);
|
|
}
|
|
} catch (_) {
|
|
state = const AuthState(isLoggedIn: false, isLoading: false);
|
|
}
|
|
}
|
|
|
|
Future<void> _loadProfile() async {
|
|
try {
|
|
final api = ref.read(apiClientProvider);
|
|
final response = await api.get('/api/user/profile');
|
|
final user = response.data['data'];
|
|
if (user != null) {
|
|
state = AuthState(isLoggedIn: true, isLoading: false,
|
|
user: UserInfo(
|
|
id: user['id'] ?? '', phone: user['phone'] ?? '',
|
|
role: user['role'] ?? state.user?.role ?? 'User',
|
|
name: user['name'], avatarUrl: user['avatarUrl'],
|
|
),
|
|
);
|
|
}
|
|
} catch (e) {
|
|
print('[Auth] loadProfile: $e');
|
|
}
|
|
}
|
|
|
|
/// 发送验证码
|
|
Future<({String? error, String? devCode})> sendSms(String phone) async {
|
|
try {
|
|
final api = ref.read(apiClientProvider);
|
|
final response = await api.post('/api/auth/send-sms', data: {'phone': phone});
|
|
return (error: null, devCode: response.data['data']?['devCode'] as String?);
|
|
} catch (e) {
|
|
return (error: '发送失败: $e', devCode: null);
|
|
}
|
|
}
|
|
|
|
/// 注册(新用户,需选身份)
|
|
Future<String?> register(String phone, String code, String role, {String? inviteCode}) async {
|
|
try {
|
|
final api = ref.read(apiClientProvider);
|
|
final response = await api.post('/api/auth/register', data: {
|
|
'phone': phone, 'smsCode': code, 'role': role,
|
|
if (inviteCode != null) 'inviteCode': inviteCode,
|
|
});
|
|
final data = response.data['data'];
|
|
if (data == null) return response.data['message'] ?? '注册失败';
|
|
|
|
await api.saveTokens(data['accessToken'], data['refreshToken']);
|
|
final user = data['user'];
|
|
state = AuthState(isLoggedIn: true, isLoading: false,
|
|
user: UserInfo(id: user['id'] ?? '', phone: user['phone'] ?? '', role: role),
|
|
);
|
|
return null;
|
|
} catch (e) {
|
|
return '注册失败: $e';
|
|
}
|
|
}
|
|
|
|
/// 登录(已有账号)
|
|
Future<String?> login(String phone, String code) async {
|
|
try {
|
|
final api = ref.read(apiClientProvider);
|
|
final response = await api.post('/api/auth/login', data: {'phone': phone, 'smsCode': code});
|
|
final data = response.data['data'];
|
|
if (data == null) return response.data['message'] ?? '登录失败';
|
|
|
|
await api.saveTokens(data['accessToken'], data['refreshToken']);
|
|
final user = data['user'];
|
|
state = AuthState(isLoggedIn: true, isLoading: false,
|
|
user: UserInfo(
|
|
id: user['id'] ?? '', phone: user['phone'] ?? '',
|
|
role: user['role'] ?? 'User', name: user['name'],
|
|
avatarUrl: user['avatarUrl'],
|
|
),
|
|
);
|
|
return null;
|
|
} catch (e) {
|
|
return '登录失败: $e';
|
|
}
|
|
}
|
|
|
|
/// 登出
|
|
Future<void> logout() async {
|
|
final api = ref.read(apiClientProvider);
|
|
final db = ref.read(localDbProvider);
|
|
final refresh = await db.read('refresh_token');
|
|
if (refresh != null) {
|
|
try { await api.post('/api/auth/logout', data: {'refreshToken': refresh}); } catch (e) { print('[Auth] logout: $e'); }
|
|
}
|
|
await api.clearTokens();
|
|
state = const AuthState(isLoggedIn: false, isLoading: false);
|
|
}
|
|
}
|
|
|
|
/// 便捷:当前用户角色
|
|
final userRoleProvider = Provider<String>((ref) {
|
|
return ref.watch(authProvider).user?.role ?? 'User';
|
|
});
|