## 后台管理页重构 - admin 端: add_doctor / doctors / home / patients 四页重构 - doctor 端: consultations / dashboard / followup_edit / followups / home / patient_detail / patients / profile / report_detail / reports / settings 十一页重构 - 新增 backoffice 共享模块: backoffice_refresh_providers + backoffice_formatters + backoffice_ui ## 后端 - AdminService 增强(分页/搜索/统计) - doctor_endpoints 微调 - appsettings.Development 调整 - 新增 admin_service_tests ## 前端其他 - 今日健康卡片 taskRow 行高 5->7(每行 44->48px) - 健康仪表盘配色定 #4FACFE 纯色蓝 - admin_drawer / doctor_drawer 微调 - api_client IP 适配 ## 启动脚本 - start-dev.bat: 加后端就绪检测(轮询 openapi 端点, 最多等 30s) ## 清理 - 删除旧品牌图(agent_welcome_abstract / login_background_v1) - 删除 app_status_badge 组件 - 删除旧 HANDOFF 文档, 新增 07-17 版 ## 测试 - 新增 backoffice_formatters / backoffice_refresh / backoffice_ui 三个测试 - app_router_test 扩展
219 lines
7.4 KiB
Dart
219 lines
7.4 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;
|
|
import '../../utils/backoffice_formatters.dart';
|
|
import '../../widgets/backoffice_ui.dart';
|
|
|
|
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;
|
|
String? _error;
|
|
|
|
@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 = [];
|
|
_error = null;
|
|
});
|
|
}
|
|
try {
|
|
final res = await ref
|
|
.read(adminServiceProvider)
|
|
.getPatients(search: _searchCtrl.text.trim(), page: _page);
|
|
if (!mounted) return;
|
|
if (res['code'] == 0) {
|
|
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;
|
|
});
|
|
} else {
|
|
setState(() {
|
|
_loading = false;
|
|
_loadingMore = false;
|
|
_error = res['message']?.toString() ?? '患者列表加载失败';
|
|
});
|
|
}
|
|
} catch (e) {
|
|
if (mounted) {
|
|
setState(() {
|
|
_loading = false;
|
|
_loadingMore = false;
|
|
_error = '患者列表加载失败';
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
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 BackofficeLoadingState(message: '正在加载患者')
|
|
: _error != null
|
|
? BackofficeErrorState(message: _error!, onRetry: _load)
|
|
: _patients.isEmpty
|
|
? const BackofficeEmptyState(
|
|
icon: Icons.people_outline,
|
|
title: '暂无患者',
|
|
description: '注册患者会显示在这里',
|
|
)
|
|
: 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 BackofficeSectionCard(
|
|
margin: const EdgeInsets.only(bottom: 8),
|
|
padding: EdgeInsets.zero,
|
|
child: ListTile(
|
|
leading: CircleAvatar(
|
|
backgroundColor: AppColors.iconBg,
|
|
child: Text(
|
|
backofficeInitial(p['name']),
|
|
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,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|