Files
AI-Health/health_app/lib/pages/admin/admin_patients_page.dart
MingNian 4d213b5a44 feat: 后端架构重构 — Endpoint→Service→Repository分层 + AI确认机制 + 异步任务持久化
- 核心业务拆分为 Endpoint → Application Service → Repository 三层
- AI写入操作必须用户确认后才写库(确认卡片机制)
- 报告/饮食/用药分析改为持久化任务队列(原子领取/重试/重启恢复)
- 运动计划修复: 连续真实日期替代周模板
- 用药提醒去重 + 通知Outbox预留
- 认证收拢到AuthService, 管理员收拢到AdminService
- AI会话加用户归属校验防串号
- 提示词调整为患者视角
- 开发假数据已关闭
- 21/21测试通过, 0警告0错误
2026-06-20 20:41:42 +08:00

212 lines
7.2 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/app_colors.dart';
import '../../providers/data_providers.dart' show adminServiceProvider;
class AdminPatientsPage extends ConsumerStatefulWidget {
const AdminPatientsPage({super.key});
@override
ConsumerState<AdminPatientsPage> createState() => _AdminPatientsPageState();
}
class _AdminPatientsPageState extends ConsumerState<AdminPatientsPage> {
final _searchCtrl = TextEditingController();
List<Map<String, dynamic>> _patients = [];
int _total = 0;
int _page = 1;
bool _loading = true;
bool _loadingMore = false;
@override
void initState() {
super.initState();
_load();
}
@override
void dispose() {
_searchCtrl.dispose();
super.dispose();
}
Future<void> _load({bool reset = true}) async {
if (reset) {
_page = 1;
setState(() {
_loading = true;
_patients = [];
});
}
try {
final res = await ref
.read(adminServiceProvider)
.getPatients(search: _searchCtrl.text.trim(), page: _page);
if (res['code'] == 0 && mounted) {
final data = res['data'] as Map<String, dynamic>? ?? {};
setState(() {
_total = data['total'] ?? 0;
final list = List<Map<String, dynamic>>.from(data['patients'] ?? []);
if (reset) {
_patients = list;
} else {
_patients.addAll(list);
}
_loading = false;
_loadingMore = false;
});
}
} catch (_) {
if (mounted) {
setState(() {
_loading = false;
_loadingMore = false;
});
}
}
}
void _loadMore() {
if (!_loadingMore && _patients.length < _total) {
setState(() {
_page++;
_loadingMore = true;
});
_load(reset: false);
}
}
@override
Widget build(BuildContext context) {
return Column(
children: [
// 搜索栏
Padding(
padding: const EdgeInsets.all(16),
child: TextField(
controller: _searchCtrl,
decoration: InputDecoration(
hintText: '搜索患者姓名或手机号',
prefixIcon: const Icon(Icons.search, color: AppColors.textHint),
suffixIcon: _searchCtrl.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
_searchCtrl.clear();
_load();
},
)
: null,
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: AppColors.border),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: AppColors.border),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: AppColors.primary),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
),
onSubmitted: (_) => _load(),
onChanged: (v) => setState(() {}),
),
),
// 患者列表
Expanded(
child: _loading
? const Center(child: CircularProgressIndicator())
: _patients.isEmpty
? const Center(
child: Text(
'暂无患者',
style: TextStyle(color: AppColors.textHint, fontSize: 16),
),
)
: NotificationListener<ScrollNotification>(
onNotification: (n) {
if (n is ScrollEndNotification &&
n.metrics.pixels >= n.metrics.maxScrollExtent - 50) {
_loadMore();
}
return false;
},
child: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: _patients.length + (_loadingMore ? 1 : 0),
itemBuilder: (_, i) {
if (i >= _patients.length) {
return const Center(
child: Padding(
padding: EdgeInsets.all(16),
child: CircularProgressIndicator(),
),
);
}
final p = _patients[i];
return Card(
margin: const EdgeInsets.only(bottom: 8),
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
side: const BorderSide(color: AppColors.border),
),
child: ListTile(
leading: CircleAvatar(
backgroundColor: AppColors.iconBg,
child: Text(
p['name']?.toString().isNotEmpty == true
? p['name']!.toString()[0]
: '?',
style: const TextStyle(color: AppColors.primary),
),
),
title: Text(
p['name'] ?? '',
style: const TextStyle(fontWeight: FontWeight.w500),
),
subtitle: Text(
p['phone'] ?? '',
style: const TextStyle(
fontSize: 13,
color: AppColors.textSecondary,
),
),
trailing: p['doctorName'] != null
? Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: AppColors.successLight,
borderRadius: BorderRadius.circular(6),
),
child: Text(
p['doctorName']!,
style: const TextStyle(
fontSize: 12,
color: AppColors.success,
),
),
)
: null,
),
);
},
),
),
),
],
);
}
}