fix: 管理员系统 + 注册流程重构 + UI改版 + 全链路修复
- 新增管理员角色(手机号12345678910),管理员可增删医生、查看患者 - 注册页重构:去掉角色选择+审核码,改为选医生+填姓名 - 医生端点按DoctorId过滤患者,Patient↔Doctor关系建立 - Doctor/DoctorProfile/User实体新增关联字段 - JSON循环引用修复(IgnoreCycles) - /api/doctors改为公开接口 - 登录闪屏修复+原生Android启动页 - 输入框全局白底+灰框 - 蓝牙重连同步修复 - Web端(doctor_web+health_app/web)全部删除 - 全局UI改版:白底企业风,新配色和组件系统 - 新品牌图标和启动图
This commit is contained in:
@@ -4,7 +4,9 @@ import '../../core/app_colors.dart';
|
||||
import '../../core/navigation_provider.dart';
|
||||
import '../../providers/auth_provider.dart';
|
||||
|
||||
final _consListProvider = FutureProvider<List<Map<String, dynamic>>>((ref) async {
|
||||
final _consListProvider = FutureProvider<List<Map<String, dynamic>>>((
|
||||
ref,
|
||||
) async {
|
||||
final api = ref.read(apiClientProvider);
|
||||
final res = await api.get('/api/doctor/consultations');
|
||||
return (res.data['data'] as List?)?.cast<Map<String, dynamic>>() ?? [];
|
||||
@@ -12,35 +14,61 @@ final _consListProvider = FutureProvider<List<Map<String, dynamic>>>((ref) async
|
||||
|
||||
class DoctorConsultationsPage extends ConsumerWidget {
|
||||
const DoctorConsultationsPage({super.key});
|
||||
@override Widget build(BuildContext context, WidgetRef ref) {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final list = ref.watch(_consListProvider);
|
||||
return list.when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (_, __) => const Center(child: Text('加载失败')),
|
||||
data: (items) => items.isEmpty
|
||||
? const Center(child: Text('暂无问诊', style: TextStyle(color: AppColors.textHint)))
|
||||
: ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: items.length,
|
||||
itemBuilder: (_, i) {
|
||||
final c = items[i];
|
||||
final status = c['status'] ?? '';
|
||||
final msg = c['lastMessage'] as Map<String, dynamic>?;
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(14)),
|
||||
child: ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: CircleAvatar(radius: 22, backgroundColor: const Color(0xFFF0F0FF), child: Text((c['patientName'] ?? '?')[0], style: const TextStyle(color: AppColors.primary))),
|
||||
title: Text(c['patientName'] ?? c['patientPhone'] ?? '', style: const TextStyle(fontWeight: FontWeight.w600)),
|
||||
subtitle: Text(msg?['content'] ?? '暂无消息', maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
trailing: _StatusBadge(status),
|
||||
onTap: () => pushRoute(ref, 'doctorChat', params: {'id': c['id']?.toString() ?? ''}),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
? const Center(
|
||||
child: Text('暂无问诊', style: TextStyle(color: AppColors.textHint)),
|
||||
)
|
||||
: ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: items.length,
|
||||
itemBuilder: (_, i) {
|
||||
final c = items[i];
|
||||
final status = c['status'] ?? '';
|
||||
final msg = c['lastMessage'] as Map<String, dynamic>?;
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: AppColors.border),
|
||||
boxShadow: AppColors.cardShadowLight,
|
||||
),
|
||||
child: ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: CircleAvatar(
|
||||
radius: 22,
|
||||
backgroundColor: AppColors.avatarBg,
|
||||
child: Text(
|
||||
(c['patientName'] ?? '?')[0],
|
||||
style: const TextStyle(color: AppColors.primary),
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
c['patientName'] ?? c['patientPhone'] ?? '',
|
||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
subtitle: Text(
|
||||
msg?['content'] ?? '暂无消息',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
trailing: _StatusBadge(status),
|
||||
onTap: () => pushRoute(
|
||||
ref,
|
||||
'doctorChat',
|
||||
params: {'id': c['id']?.toString() ?? ''},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -48,14 +76,29 @@ class DoctorConsultationsPage extends ConsumerWidget {
|
||||
class _StatusBadge extends StatelessWidget {
|
||||
final String status;
|
||||
const _StatusBadge(this.status);
|
||||
@override Widget build(BuildContext context) {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final (label, color) = switch (status) {
|
||||
'AiTalking' => ('AI中', const Color(0xFF6366F1)),
|
||||
'AiTalking' => ('AI中', AppColors.primary),
|
||||
'WaitingDoctor' => ('等待医生', const Color(0xFFF59E0B)),
|
||||
'DoctorReplied' => ('已回复', const Color(0xFF10B981)),
|
||||
'Closed' => ('已关闭', const Color(0xFF9CA3AF)),
|
||||
_ => (status, const Color(0xFF9CA3AF)),
|
||||
};
|
||||
return Container(padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration(color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(8)), child: Text(label, style: TextStyle(fontSize: 12, color: color, fontWeight: FontWeight.w500)));
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: color,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,16 +27,38 @@ class DoctorDashboardPage extends ConsumerWidget {
|
||||
Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
decoration: BoxDecoration(color: const Color(0xFFE0F2FE), borderRadius: BorderRadius.circular(12)),
|
||||
child: Row(children: [
|
||||
const Icon(Icons.info_outline, color: Color(0xFF0891B2), size: 20),
|
||||
const SizedBox(width: 10),
|
||||
const Expanded(child: Text('请完善个人信息', style: TextStyle(color: Color(0xFF0891B2), fontSize: 14))),
|
||||
GestureDetector(
|
||||
onTap: () => {},
|
||||
child: const Text('去完善', style: TextStyle(color: Color(0xFF0891B2), fontWeight: FontWeight.w600)),
|
||||
),
|
||||
]),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.infoLight,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: AppColors.border),
|
||||
boxShadow: AppColors.cardShadowLight,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.info_outline,
|
||||
color: AppColors.primary,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
const Expanded(
|
||||
child: Text(
|
||||
'请完善个人信息',
|
||||
style: TextStyle(color: AppColors.primary, fontSize: 14),
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () => {},
|
||||
child: const Text(
|
||||
'去完善',
|
||||
style: TextStyle(
|
||||
color: AppColors.primary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
dash.when(
|
||||
@@ -49,71 +71,122 @@ class DoctorDashboardPage extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContent(BuildContext context, WidgetRef ref, Map<String, dynamic>? data) {
|
||||
Widget _buildContent(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
Map<String, dynamic>? data,
|
||||
) {
|
||||
final stats = data?['stats'] as Map<String, dynamic>? ?? {};
|
||||
|
||||
return Column(children: [
|
||||
// 统计卡片
|
||||
Row(children: [
|
||||
Expanded(child: _StatCard('患者总数', '${stats['totalPatients'] ?? 0}', Icons.people, const Color(0xFF3B82F6), () {
|
||||
ref.read(doctorPageProvider.notifier).set('patients');
|
||||
})),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(child: _StatCard('进行中问诊', '${stats['activeConsultations'] ?? 0}', Icons.chat, const Color(0xFF10B981), () {
|
||||
ref.read(doctorPageProvider.notifier).set('consultations');
|
||||
})),
|
||||
]),
|
||||
const SizedBox(height: 10),
|
||||
Row(children: [
|
||||
Expanded(child: _StatCard('待审核报告', '${stats['pendingReports'] ?? 0}', Icons.description, const Color(0xFFF59E0B), () {
|
||||
ref.read(doctorPageProvider.notifier).set('reports');
|
||||
})),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(child: _StatCard('今日随访', '${stats['todayFollowUps'] ?? 0}', Icons.event_note, const Color(0xFFEF4444), () {
|
||||
ref.read(doctorPageProvider.notifier).set('followups');
|
||||
})),
|
||||
]),
|
||||
const SizedBox(height: 20),
|
||||
return Column(
|
||||
children: [
|
||||
// 统计卡片
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _StatCard(
|
||||
'患者总数',
|
||||
'${stats['totalPatients'] ?? 0}',
|
||||
Icons.people,
|
||||
const Color(0xFF3B82F6),
|
||||
() {
|
||||
ref.read(doctorPageProvider.notifier).set('patients');
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: _StatCard(
|
||||
'进行中问诊',
|
||||
'${stats['activeConsultations'] ?? 0}',
|
||||
Icons.chat,
|
||||
const Color(0xFF10B981),
|
||||
() {
|
||||
ref.read(doctorPageProvider.notifier).set('consultations');
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _StatCard(
|
||||
'待审核报告',
|
||||
'${stats['pendingReports'] ?? 0}',
|
||||
Icons.description,
|
||||
const Color(0xFFF59E0B),
|
||||
() {
|
||||
ref.read(doctorPageProvider.notifier).set('reports');
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: _StatCard(
|
||||
'今日随访',
|
||||
'${stats['todayFollowUps'] ?? 0}',
|
||||
Icons.event_note,
|
||||
const Color(0xFFEF4444),
|
||||
() {
|
||||
ref.read(doctorPageProvider.notifier).set('followups');
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// 待办:未回复问诊
|
||||
_TodoSection(
|
||||
title: '待回复问诊',
|
||||
icon: Icons.chat_outlined,
|
||||
color: const Color(0xFF10B981),
|
||||
items: (data?['pendingConsultations'] as List?)?.cast<Map<String, dynamic>>() ?? [],
|
||||
itemLabel: (m) => m['patientName'] ?? '',
|
||||
itemSubtitle: (m) => '待回复',
|
||||
onTap: (m) {
|
||||
ref.read(doctorPageProvider.notifier).set('consultations');
|
||||
},
|
||||
),
|
||||
// 待办:未回复问诊
|
||||
_TodoSection(
|
||||
title: '待回复问诊',
|
||||
icon: Icons.chat_outlined,
|
||||
color: const Color(0xFF10B981),
|
||||
items:
|
||||
(data?['pendingConsultations'] as List?)
|
||||
?.cast<Map<String, dynamic>>() ??
|
||||
[],
|
||||
itemLabel: (m) => m['patientName'] ?? '',
|
||||
itemSubtitle: (m) => '待回复',
|
||||
onTap: (m) {
|
||||
ref.read(doctorPageProvider.notifier).set('consultations');
|
||||
},
|
||||
),
|
||||
|
||||
// 待办:待审核报告
|
||||
_TodoSection(
|
||||
title: '待审核报告',
|
||||
icon: Icons.description_outlined,
|
||||
color: const Color(0xFFF59E0B),
|
||||
items: (data?['pendingReports'] as List?)?.cast<Map<String, dynamic>>() ?? [],
|
||||
itemLabel: (m) => '${m['patientName'] ?? ''}',
|
||||
itemSubtitle: (m) => '${m['category'] ?? ''}',
|
||||
onTap: (m) {
|
||||
ref.read(doctorPageProvider.notifier).set('reports');
|
||||
},
|
||||
),
|
||||
// 待办:待审核报告
|
||||
_TodoSection(
|
||||
title: '待审核报告',
|
||||
icon: Icons.description_outlined,
|
||||
color: const Color(0xFFF59E0B),
|
||||
items:
|
||||
(data?['pendingReports'] as List?)
|
||||
?.cast<Map<String, dynamic>>() ??
|
||||
[],
|
||||
itemLabel: (m) => '${m['patientName'] ?? ''}',
|
||||
itemSubtitle: (m) => '${m['category'] ?? ''}',
|
||||
onTap: (m) {
|
||||
ref.read(doctorPageProvider.notifier).set('reports');
|
||||
},
|
||||
),
|
||||
|
||||
// 今日随访
|
||||
_TodoSection(
|
||||
title: '今日随访',
|
||||
icon: Icons.event_note_outlined,
|
||||
color: const Color(0xFFEF4444),
|
||||
items: (data?['todayFollowUps'] as List?)?.cast<Map<String, dynamic>>() ?? [],
|
||||
itemLabel: (m) => '${m['title'] ?? ''}',
|
||||
itemSubtitle: (m) => '${m['patientName'] ?? ''}',
|
||||
onTap: (m) {
|
||||
ref.read(doctorPageProvider.notifier).set('followups');
|
||||
},
|
||||
),
|
||||
]);
|
||||
// 今日随访
|
||||
_TodoSection(
|
||||
title: '今日随访',
|
||||
icon: Icons.event_note_outlined,
|
||||
color: const Color(0xFFEF4444),
|
||||
items:
|
||||
(data?['todayFollowUps'] as List?)
|
||||
?.cast<Map<String, dynamic>>() ??
|
||||
[],
|
||||
itemLabel: (m) => '${m['title'] ?? ''}',
|
||||
itemSubtitle: (m) => '${m['patientName'] ?? ''}',
|
||||
onTap: (m) {
|
||||
ref.read(doctorPageProvider.notifier).set('followups');
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,24 +199,52 @@ class _StatCard extends StatelessWidget {
|
||||
|
||||
const _StatCard(this.label, this.value, this.icon, this.color, this.onTap);
|
||||
|
||||
@override Widget build(BuildContext context) {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(14)),
|
||||
child: Row(children: [
|
||||
Container(
|
||||
width: 40, height: 40,
|
||||
decoration: BoxDecoration(color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(10)),
|
||||
child: Icon(icon, color: color, size: 20),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text(value, style: const TextStyle(fontSize: 22, fontWeight: FontWeight.w700, color: AppColors.textPrimary)),
|
||||
Text(label, style: const TextStyle(fontSize: 12, color: AppColors.textHint)),
|
||||
]),
|
||||
]),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: AppColors.border),
|
||||
boxShadow: AppColors.cardShadowLight,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(icon, color: color, size: 20),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.textHint,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -158,38 +259,98 @@ class _TodoSection extends StatelessWidget {
|
||||
final String Function(Map<String, dynamic>) itemSubtitle;
|
||||
final void Function(Map<String, dynamic>) onTap;
|
||||
|
||||
const _TodoSection({required this.title, required this.icon, required this.color, required this.items, required this.itemLabel, required this.itemSubtitle, required this.onTap});
|
||||
const _TodoSection({
|
||||
required this.title,
|
||||
required this.icon,
|
||||
required this.color,
|
||||
required this.items,
|
||||
required this.itemLabel,
|
||||
required this.itemSubtitle,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override Widget build(BuildContext context) {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (items.isEmpty) return const SizedBox.shrink();
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(14)),
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Row(children: [
|
||||
Icon(icon, size: 18, color: color),
|
||||
const SizedBox(width: 6),
|
||||
Text(title, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: AppColors.textPrimary)),
|
||||
const Spacer(),
|
||||
Text('${items.length}项', style: const TextStyle(fontSize: 12, color: AppColors.textHint)),
|
||||
]),
|
||||
const SizedBox(height: 8),
|
||||
...items.take(5).map((m) => GestureDetector(
|
||||
onTap: () => onTap(m),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
decoration: const BoxDecoration(border: Border(top: BorderSide(color: Color(0xFFF5F5F5)))),
|
||||
child: Row(children: [
|
||||
Expanded(child: Text(itemLabel(m), style: const TextStyle(fontSize: 14, color: AppColors.textPrimary))),
|
||||
Text(itemSubtitle(m), style: const TextStyle(fontSize: 12, color: AppColors.textHint)),
|
||||
const SizedBox(width: 4),
|
||||
const Icon(Icons.chevron_right, size: 16, color: AppColors.textHint),
|
||||
]),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: AppColors.border),
|
||||
boxShadow: AppColors.cardShadowLight,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(icon, size: 18, color: color),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
'${items.length}项',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.textHint,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)),
|
||||
]),
|
||||
const SizedBox(height: 8),
|
||||
...items
|
||||
.take(5)
|
||||
.map(
|
||||
(m) => GestureDetector(
|
||||
onTap: () => onTap(m),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(
|
||||
top: BorderSide(color: AppColors.divider),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
itemLabel(m),
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
itemSubtitle(m),
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.textHint,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
const Icon(
|
||||
Icons.chevron_right,
|
||||
size: 16,
|
||||
color: AppColors.textHint,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,10 +13,13 @@ final _ptsSimple = FutureProvider<List<Map<String, dynamic>>>((ref) async {
|
||||
class DoctorFollowUpEditPage extends ConsumerStatefulWidget {
|
||||
final String? id;
|
||||
const DoctorFollowUpEditPage({super.key, this.id});
|
||||
@override ConsumerState<DoctorFollowUpEditPage> createState() => _DoctorFollowUpEditPageState();
|
||||
@override
|
||||
ConsumerState<DoctorFollowUpEditPage> createState() =>
|
||||
_DoctorFollowUpEditPageState();
|
||||
}
|
||||
|
||||
class _DoctorFollowUpEditPageState extends ConsumerState<DoctorFollowUpEditPage> {
|
||||
class _DoctorFollowUpEditPageState
|
||||
extends ConsumerState<DoctorFollowUpEditPage> {
|
||||
final _titleCtrl = TextEditingController();
|
||||
final _notesCtrl = TextEditingController();
|
||||
String? _pid;
|
||||
@@ -27,9 +30,15 @@ class _DoctorFollowUpEditPageState extends ConsumerState<DoctorFollowUpEditPage>
|
||||
|
||||
bool get isEdit => widget.id != null && widget.id!.isNotEmpty;
|
||||
|
||||
@override void dispose() { _titleCtrl.dispose(); _notesCtrl.dispose(); super.dispose(); }
|
||||
@override
|
||||
void dispose() {
|
||||
_titleCtrl.dispose();
|
||||
_notesCtrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override Widget build(BuildContext context) {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (isEdit && !_loaded) {
|
||||
ref.watch(_fupDetailForEdit(widget.id!)).whenData((d) {
|
||||
if (d != null) {
|
||||
@@ -38,7 +47,10 @@ class _DoctorFollowUpEditPageState extends ConsumerState<DoctorFollowUpEditPage>
|
||||
_notesCtrl.text = d['notes'] ?? '';
|
||||
_pid = d['userId']?.toString();
|
||||
final at = DateTime.tryParse(d['scheduledAt']?.toString() ?? '');
|
||||
if (at != null) { _date = at; _time = TimeOfDay.fromDateTime(at); }
|
||||
if (at != null) {
|
||||
_date = at;
|
||||
_time = TimeOfDay.fromDateTime(at);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -46,114 +58,245 @@ class _DoctorFollowUpEditPageState extends ConsumerState<DoctorFollowUpEditPage>
|
||||
final pts = ref.watch(_ptsSimple);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF5F5F5),
|
||||
backgroundColor: AppColors.background,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.white, elevation: 0,
|
||||
title: Text(isEdit ? '编辑随访' : '新建随访', style: const TextStyle(color: AppColors.textPrimary)),
|
||||
leading: IconButton(icon: const Icon(Icons.arrow_back, color: AppColors.textPrimary), onPressed: () => popRoute(ref)),
|
||||
),
|
||||
body: ListView(padding: const EdgeInsets.all(16), children: [
|
||||
const Text('患者', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600)),
|
||||
const SizedBox(height: 8),
|
||||
pts.when(
|
||||
loading: () => const LinearProgressIndicator(),
|
||||
error: (_, __) => const Text('加载失败'),
|
||||
data: (list) => _dropdown(list),
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
title: Text(
|
||||
isEdit ? '编辑随访' : '新建随访',
|
||||
style: const TextStyle(color: AppColors.textPrimary),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_input('随访标题', _titleCtrl, hint: '例:术后一个月复查'),
|
||||
const SizedBox(height: 16),
|
||||
const Text('随访时间', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600)),
|
||||
const SizedBox(height: 8),
|
||||
_dateTimePicker(),
|
||||
const SizedBox(height: 16),
|
||||
_input('备注', _notesCtrl, hint: '随访备注(可选)', maxLines: 4),
|
||||
const SizedBox(height: 28),
|
||||
_submitBtn(),
|
||||
]),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back, color: AppColors.textPrimary),
|
||||
onPressed: () => popRoute(ref),
|
||||
),
|
||||
),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
const Text(
|
||||
'患者',
|
||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
pts.when(
|
||||
loading: () => const LinearProgressIndicator(),
|
||||
error: (_, __) => const Text('加载失败'),
|
||||
data: (list) => _dropdown(list),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_input('随访标题', _titleCtrl, hint: '例:术后一个月复查'),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'随访时间',
|
||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_dateTimePicker(),
|
||||
const SizedBox(height: 16),
|
||||
_input('备注', _notesCtrl, hint: '随访备注(可选)', maxLines: 4),
|
||||
const SizedBox(height: 28),
|
||||
_submitBtn(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _dropdown(List<Map<String, dynamic>> list) => Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(14)),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: AppColors.border),
|
||||
),
|
||||
child: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<String?>(
|
||||
isExpanded: true,
|
||||
value: _pid,
|
||||
hint: const Text('选择患者', style: TextStyle(color: AppColors.textHint)),
|
||||
items: [
|
||||
const DropdownMenuItem<String?>(value: null, child: Text('选择患者', style: TextStyle(color: AppColors.textHint))),
|
||||
...list.map((p) => DropdownMenuItem<String?>(value: p['id']?.toString(), child: Text('${p['name'] ?? p['phone']} (${p['phone']})', style: const TextStyle(fontSize: 15)))),
|
||||
const DropdownMenuItem<String?>(
|
||||
value: null,
|
||||
child: Text('选择患者', style: TextStyle(color: AppColors.textHint)),
|
||||
),
|
||||
...list.map(
|
||||
(p) => DropdownMenuItem<String?>(
|
||||
value: p['id']?.toString(),
|
||||
child: Text(
|
||||
'${p['name'] ?? p['phone']} (${p['phone']})',
|
||||
style: const TextStyle(fontSize: 15),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
onChanged: (v) => setState(() { _pid = v; }),
|
||||
onChanged: (v) => setState(() {
|
||||
_pid = v;
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Widget _input(String label, TextEditingController ctrl, {String? hint, int maxLines = 1}) => Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text(label, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600)),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(14)),
|
||||
child: TextField(controller: ctrl, maxLines: maxLines, decoration: InputDecoration(hintText: hint, border: InputBorder.none)),
|
||||
),
|
||||
]);
|
||||
Widget _input(
|
||||
String label,
|
||||
TextEditingController ctrl, {
|
||||
String? hint,
|
||||
int maxLines = 1,
|
||||
}) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: AppColors.border),
|
||||
),
|
||||
child: TextField(
|
||||
controller: ctrl,
|
||||
maxLines: maxLines,
|
||||
decoration: InputDecoration(
|
||||
hintText: hint,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
borderSide: const BorderSide(color: Colors.transparent),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
Widget _dateTimePicker() => Row(children: [
|
||||
Expanded(child: GestureDetector(
|
||||
onTap: () async {
|
||||
final d = await showDatePicker(context: context, initialDate: _date, firstDate: DateTime.now(), lastDate: DateTime.now().add(const Duration(days: 365)));
|
||||
if (d != null) setState(() => _date = d);
|
||||
},
|
||||
child: _pickerBox('${_date.year}-${_date.month.toString().padLeft(2, '0')}-${_date.day.toString().padLeft(2, '0')}'),
|
||||
)),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: GestureDetector(
|
||||
onTap: () async {
|
||||
final t = await showTimePicker(context: context, initialTime: _time);
|
||||
if (t != null) setState(() => _time = t);
|
||||
},
|
||||
child: _pickerBox('${_time.hour.toString().padLeft(2, '0')}:${_time.minute.toString().padLeft(2, '0')}'),
|
||||
)),
|
||||
]);
|
||||
Widget _dateTimePicker() => Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () async {
|
||||
final d = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: _date,
|
||||
firstDate: DateTime.now(),
|
||||
lastDate: DateTime.now().add(const Duration(days: 365)),
|
||||
);
|
||||
if (d != null) setState(() => _date = d);
|
||||
},
|
||||
child: _pickerBox(
|
||||
'${_date.year}-${_date.month.toString().padLeft(2, '0')}-${_date.day.toString().padLeft(2, '0')}',
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () async {
|
||||
final t = await showTimePicker(
|
||||
context: context,
|
||||
initialTime: _time,
|
||||
);
|
||||
if (t != null) setState(() => _time = t);
|
||||
},
|
||||
child: _pickerBox(
|
||||
'${_time.hour.toString().padLeft(2, '0')}:${_time.minute.toString().padLeft(2, '0')}',
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
Widget _pickerBox(String text) => Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(14)),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: AppColors.border),
|
||||
),
|
||||
child: Center(child: Text(text, style: const TextStyle(fontSize: 15))),
|
||||
);
|
||||
|
||||
Widget _submitBtn() => SizedBox(width: double.infinity, child: ElevatedButton(
|
||||
onPressed: _saving ? null : _save,
|
||||
style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFF0891B2), foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14))),
|
||||
child: _saving ? const CircularProgressIndicator(color: Colors.white) : Text(isEdit ? '保存修改' : '创建随访', style: const TextStyle(fontSize: 16)),
|
||||
));
|
||||
Widget _submitBtn() => SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: _saving ? null : _save,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primary,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
|
||||
),
|
||||
child: _saving
|
||||
? const CircularProgressIndicator(color: Colors.white)
|
||||
: Text(
|
||||
isEdit ? '保存修改' : '创建随访',
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Future<void> _save() async {
|
||||
if (_pid == null) { _snack('请选择患者'); return; }
|
||||
if (_titleCtrl.text.trim().isEmpty) { _snack('请输入随访标题'); return; }
|
||||
if (_pid == null) {
|
||||
_snack('请选择患者');
|
||||
return;
|
||||
}
|
||||
if (_titleCtrl.text.trim().isEmpty) {
|
||||
_snack('请输入随访标题');
|
||||
return;
|
||||
}
|
||||
setState(() => _saving = true);
|
||||
final at = DateTime(_date.year, _date.month, _date.day, _time.hour, _time.minute);
|
||||
final at = DateTime(
|
||||
_date.year,
|
||||
_date.month,
|
||||
_date.day,
|
||||
_time.hour,
|
||||
_time.minute,
|
||||
);
|
||||
try {
|
||||
final api = ref.read(apiClientProvider);
|
||||
final data = {'userId': _pid, 'title': _titleCtrl.text.trim(), 'scheduledAt': at.toIso8601String(), 'notes': _notesCtrl.text.trim().isEmpty ? null : _notesCtrl.text.trim()};
|
||||
if (isEdit) { await api.put('/api/doctor/follow-ups/${widget.id}', data: data); }
|
||||
else { await api.post('/api/doctor/follow-ups', data: data); }
|
||||
if (mounted) { _snack(isEdit ? '修改成功' : '创建成功', ok: true); popRoute(ref); }
|
||||
} catch (e) { if (mounted) _snack('保存失败: $e'); }
|
||||
finally { if (mounted) setState(() => _saving = false); }
|
||||
final data = {
|
||||
'userId': _pid,
|
||||
'title': _titleCtrl.text.trim(),
|
||||
'scheduledAt': at.toIso8601String(),
|
||||
'notes': _notesCtrl.text.trim().isEmpty ? null : _notesCtrl.text.trim(),
|
||||
};
|
||||
if (isEdit) {
|
||||
await api.put('/api/doctor/follow-ups/${widget.id}', data: data);
|
||||
} else {
|
||||
await api.post('/api/doctor/follow-ups', data: data);
|
||||
}
|
||||
if (mounted) {
|
||||
_snack(isEdit ? '修改成功' : '创建成功', ok: true);
|
||||
popRoute(ref);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) _snack('保存失败: $e');
|
||||
} finally {
|
||||
if (mounted) setState(() => _saving = false);
|
||||
}
|
||||
}
|
||||
|
||||
void _snack(String msg, {bool ok = false}) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg), backgroundColor: ok ? AppColors.success : AppColors.error));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(msg),
|
||||
backgroundColor: ok ? AppColors.success : AppColors.error,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final _fupDetailForEdit = FutureProvider.family<Map<String, dynamic>?, String>((ref, id) async {
|
||||
final _fupDetailForEdit = FutureProvider.family<Map<String, dynamic>?, String>((
|
||||
ref,
|
||||
id,
|
||||
) async {
|
||||
final api = ref.read(apiClientProvider);
|
||||
final res = await api.get('/api/doctor/follow-ups');
|
||||
final items = (res.data['data'] as List?)?.cast<Map<String, dynamic>>() ?? [];
|
||||
return items.cast<Map<String, dynamic>?>().firstWhere((f) => f?['id']?.toString() == id, orElse: () => null);
|
||||
return items.cast<Map<String, dynamic>?>().firstWhere(
|
||||
(f) => f?['id']?.toString() == id,
|
||||
orElse: () => null,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -12,100 +12,253 @@ final _fupRefresh = FutureProvider<String?>((ref) async {
|
||||
return null;
|
||||
});
|
||||
|
||||
final _fupList = NotifierProvider<FupListN, List<Map<String, dynamic>>>(FupListN.new);
|
||||
final _fupList = NotifierProvider<FupListN, List<Map<String, dynamic>>>(
|
||||
FupListN.new,
|
||||
);
|
||||
|
||||
class FupListN extends Notifier<List<Map<String, dynamic>>> {
|
||||
@override List<Map<String, dynamic>> build() => [];
|
||||
@override
|
||||
List<Map<String, dynamic>> build() => [];
|
||||
void replace(List<Map<String, dynamic>> v) => state = v;
|
||||
void markDone(String id) => state = state.map((f) => f['id'] == id ? {...f, 'status': 'Completed'} : f).toList();
|
||||
void markDone(String id) => state = state
|
||||
.map((f) => f['id'] == id ? {...f, 'status': 'Completed'} : f)
|
||||
.toList();
|
||||
void remove(String id) => state = state.where((f) => f['id'] != id).toList();
|
||||
}
|
||||
|
||||
class DoctorFollowupsPage extends ConsumerStatefulWidget {
|
||||
const DoctorFollowupsPage({super.key});
|
||||
@override ConsumerState<DoctorFollowupsPage> createState() => _DoctorFollowupsPageState();
|
||||
@override
|
||||
ConsumerState<DoctorFollowupsPage> createState() =>
|
||||
_DoctorFollowupsPageState();
|
||||
}
|
||||
|
||||
class _DoctorFollowupsPageState extends ConsumerState<DoctorFollowupsPage> {
|
||||
String _filter = '';
|
||||
|
||||
@override void initState() { super.initState(); Future.microtask(() => ref.read(_fupRefresh)); }
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
Future.microtask(() => ref.read(_fupRefresh));
|
||||
}
|
||||
|
||||
@override Widget build(BuildContext context) {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var items = ref.watch(_fupList);
|
||||
if (_filter == 'Upcoming') items = items.where((f) => f['status'] == 'Upcoming').toList();
|
||||
else if (_filter == 'Completed') items = items.where((f) => f['status'] == 'Completed').toList();
|
||||
if (_filter == 'Upcoming')
|
||||
items = items.where((f) => f['status'] == 'Upcoming').toList();
|
||||
else if (_filter == 'Completed')
|
||||
items = items.where((f) => f['status'] == 'Completed').toList();
|
||||
|
||||
return Column(children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(children: [
|
||||
_Filt('全部', _filter == '', () => setState(() => _filter = '')),
|
||||
const SizedBox(width: 8),
|
||||
_Filt('待完成', _filter == 'Upcoming', () => setState(() => _filter = 'Upcoming')),
|
||||
const SizedBox(width: 8),
|
||||
_Filt('已完成', _filter == 'Completed', () => setState(() => _filter = 'Completed')),
|
||||
const Spacer(),
|
||||
IconButton(icon: const Icon(Icons.add, color: AppColors.primary), onPressed: () => pushRoute(ref, 'doctorFollowUpEdit')),
|
||||
]),
|
||||
),
|
||||
Expanded(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () => ref.refresh(_fupRefresh.future),
|
||||
child: items.isEmpty
|
||||
? ListView(children: const [SizedBox(height: 100), Center(child: Text('暂无随访', style: TextStyle(color: AppColors.textHint)))])
|
||||
: ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
itemCount: items.length,
|
||||
itemBuilder: (_, i) {
|
||||
final f = items[i];
|
||||
final upcoming = f['status'] == 'Upcoming';
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(14)),
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Row(children: [
|
||||
Expanded(child: Text(f['title'] ?? '', style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 15))),
|
||||
Container(padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration(color: (upcoming ? const Color(0xFFF59E0B) : const Color(0xFF10B981)).withOpacity(0.1), borderRadius: BorderRadius.circular(8)), child: Text(upcoming ? '待完成' : '已完成', style: TextStyle(fontSize: 12, color: upcoming ? const Color(0xFFF59E0B) : const Color(0xFF10B981)))),
|
||||
]),
|
||||
const SizedBox(height: 4),
|
||||
Text('${f['patientName'] ?? ''} · ${(f['scheduledAt'] ?? '').toString().substring(0, 16)}', style: const TextStyle(fontSize: 13, color: AppColors.textHint)),
|
||||
if (f['notes'] != null) Padding(padding: const EdgeInsets.only(top: 4), child: Text(f['notes']!, style: const TextStyle(fontSize: 13, color: AppColors.textSecondary))),
|
||||
if (upcoming) ...[
|
||||
const SizedBox(height: 8),
|
||||
Row(mainAxisAlignment: MainAxisAlignment.end, children: [
|
||||
TextButton(onPressed: () => pushRoute(ref, 'doctorFollowUpEdit', params: {'id': f['id']?.toString() ?? ''}), child: const Text('编辑', style: TextStyle(fontSize: 13))),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await ref.read(apiClientProvider).put('/api/doctor/follow-ups/${f['id']}', data: {'status': 'Completed'});
|
||||
ref.read(_fupList.notifier).markDone(f['id']?.toString() ?? '');
|
||||
},
|
||||
child: const Text('完成', style: TextStyle(fontSize: 13, color: Color(0xFF10B981))),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await ref.read(apiClientProvider).delete('/api/doctor/follow-ups/${f['id']}');
|
||||
ref.read(_fupList.notifier).remove(f['id']?.toString() ?? '');
|
||||
},
|
||||
child: const Text('删除', style: TextStyle(fontSize: 13, color: AppColors.error)),
|
||||
),
|
||||
]),
|
||||
],
|
||||
]),
|
||||
);
|
||||
},
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
_Filt('全部', _filter == '', () => setState(() => _filter = '')),
|
||||
const SizedBox(width: 8),
|
||||
_Filt(
|
||||
'待完成',
|
||||
_filter == 'Upcoming',
|
||||
() => setState(() => _filter = 'Upcoming'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_Filt(
|
||||
'已完成',
|
||||
_filter == 'Completed',
|
||||
() => setState(() => _filter = 'Completed'),
|
||||
),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add, color: AppColors.primary),
|
||||
onPressed: () => pushRoute(ref, 'doctorFollowUpEdit'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
]);
|
||||
Expanded(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () => ref.refresh(_fupRefresh.future),
|
||||
child: items.isEmpty
|
||||
? ListView(
|
||||
children: const [
|
||||
SizedBox(height: 100),
|
||||
Center(
|
||||
child: Text(
|
||||
'暂无随访',
|
||||
style: TextStyle(color: AppColors.textHint),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
itemCount: items.length,
|
||||
itemBuilder: (_, i) {
|
||||
final f = items[i];
|
||||
final upcoming = f['status'] == 'Upcoming';
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: AppColors.border),
|
||||
boxShadow: AppColors.cardShadowLight,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
f['title'] ?? '',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 3,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
(upcoming
|
||||
? AppColors.warning
|
||||
: AppColors.success)
|
||||
.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
upcoming ? '待完成' : '已完成',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: upcoming
|
||||
? AppColors.warning
|
||||
: AppColors.success,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${f['patientName'] ?? ''} · ${(f['scheduledAt'] ?? '').toString().substring(0, 16)}',
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: AppColors.textHint,
|
||||
),
|
||||
),
|
||||
if (f['notes'] != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
child: Text(
|
||||
f['notes']!,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (upcoming) ...[
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => pushRoute(
|
||||
ref,
|
||||
'doctorFollowUpEdit',
|
||||
params: {'id': f['id']?.toString() ?? ''},
|
||||
),
|
||||
child: const Text(
|
||||
'编辑',
|
||||
style: TextStyle(fontSize: 13),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await ref
|
||||
.read(apiClientProvider)
|
||||
.put(
|
||||
'/api/doctor/follow-ups/${f['id']}',
|
||||
data: {'status': 'Completed'},
|
||||
);
|
||||
ref
|
||||
.read(_fupList.notifier)
|
||||
.markDone(f['id']?.toString() ?? '');
|
||||
},
|
||||
child: const Text(
|
||||
'完成',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF10B981),
|
||||
),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await ref
|
||||
.read(apiClientProvider)
|
||||
.delete(
|
||||
'/api/doctor/follow-ups/${f['id']}',
|
||||
);
|
||||
ref
|
||||
.read(_fupList.notifier)
|
||||
.remove(f['id']?.toString() ?? '');
|
||||
},
|
||||
child: const Text(
|
||||
'删除',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: AppColors.error,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Filt extends StatelessWidget {
|
||||
final String label; final bool active; final VoidCallback onTap;
|
||||
final String label;
|
||||
final bool active;
|
||||
final VoidCallback onTap;
|
||||
const _Filt(this.label, this.active, this.onTap);
|
||||
@override Widget build(BuildContext context) => GestureDetector(
|
||||
@override
|
||||
Widget build(BuildContext context) => GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 7), decoration: BoxDecoration(color: active ? AppColors.primary : Colors.white, borderRadius: BorderRadius.circular(20), border: Border.all(color: active ? AppColors.primary : const Color(0xFFE5E7EB))), child: Text(label, style: TextStyle(fontSize: 13, color: active ? Colors.white : AppColors.textSecondary))),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 7),
|
||||
decoration: BoxDecoration(
|
||||
color: active ? AppColors.primary : Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(
|
||||
color: active ? AppColors.primary : AppColors.border,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: active ? Colors.white : AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../core/app_colors.dart';
|
||||
import '../../core/navigation_provider.dart';
|
||||
import '../../widgets/doctor_drawer.dart';
|
||||
import 'doctor_dashboard_page.dart';
|
||||
import 'doctor_patients_page.dart';
|
||||
@@ -22,11 +21,15 @@ class DoctorHomePage extends ConsumerWidget {
|
||||
if (!didPop) ref.read(doctorPageProvider.notifier).set('dashboard');
|
||||
},
|
||||
child: Scaffold(
|
||||
backgroundColor: const Color(0xFFF5F5F5),
|
||||
backgroundColor: AppColors.background,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
title: Text(_titleFor(page), style: const TextStyle(color: AppColors.textPrimary)),
|
||||
surfaceTintColor: Colors.transparent,
|
||||
title: Text(
|
||||
_titleFor(page),
|
||||
style: const TextStyle(color: AppColors.textPrimary),
|
||||
),
|
||||
leading: Builder(
|
||||
builder: (ctx) => IconButton(
|
||||
icon: const Icon(Icons.menu, color: AppColors.textPrimary),
|
||||
@@ -59,9 +62,12 @@ class DoctorHomePage extends ConsumerWidget {
|
||||
};
|
||||
}
|
||||
|
||||
final doctorPageProvider = NotifierProvider<DoctorPageNotifier, String>(DoctorPageNotifier.new);
|
||||
final doctorPageProvider = NotifierProvider<DoctorPageNotifier, String>(
|
||||
DoctorPageNotifier.new,
|
||||
);
|
||||
|
||||
class DoctorPageNotifier extends Notifier<String> {
|
||||
@override String build() => 'dashboard';
|
||||
@override
|
||||
String build() => 'dashboard';
|
||||
void set(String page) => state = page;
|
||||
}
|
||||
|
||||
@@ -4,27 +4,41 @@ import '../../core/app_colors.dart';
|
||||
import '../../core/navigation_provider.dart';
|
||||
import '../../providers/auth_provider.dart';
|
||||
|
||||
final _patientDetailProvider = FutureProvider.family<Map<String, dynamic>?, String>((ref, id) async {
|
||||
final api = ref.read(apiClientProvider);
|
||||
final res = await api.get('/api/doctor/patients/$id');
|
||||
return res.data['data'] as Map<String, dynamic>?;
|
||||
});
|
||||
final _patientDetailProvider =
|
||||
FutureProvider.family<Map<String, dynamic>?, String>((ref, id) async {
|
||||
final api = ref.read(apiClientProvider);
|
||||
final res = await api.get('/api/doctor/patients/$id');
|
||||
return res.data['data'] as Map<String, dynamic>?;
|
||||
});
|
||||
|
||||
class DoctorPatientDetailPage extends ConsumerWidget {
|
||||
final String id;
|
||||
const DoctorPatientDetailPage({super.key, required this.id});
|
||||
|
||||
@override Widget build(BuildContext context, WidgetRef ref) {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final detail = ref.watch(_patientDetailProvider(id));
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF5F5F5),
|
||||
appBar: AppBar(backgroundColor: Colors.white, elevation: 0, title: const Text('患者详情', style: TextStyle(color: AppColors.textPrimary)),
|
||||
leading: IconButton(icon: const Icon(Icons.arrow_back, color: AppColors.textPrimary), onPressed: () => popRoute(ref)),
|
||||
backgroundColor: AppColors.background,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
title: const Text(
|
||||
'患者详情',
|
||||
style: TextStyle(color: AppColors.textPrimary),
|
||||
),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back, color: AppColors.textPrimary),
|
||||
onPressed: () => popRoute(ref),
|
||||
),
|
||||
),
|
||||
body: detail.when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (_, __) => const Center(child: Text('加载失败')),
|
||||
data: (data) => data == null ? const Center(child: Text('患者不存在')) : _buildBody(data),
|
||||
data: (data) => data == null
|
||||
? const Center(child: Text('患者不存在'))
|
||||
: _buildBody(data),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -40,95 +54,241 @@ class DoctorPatientDetailPage extends ConsumerWidget {
|
||||
final diet = data['dietRecords'] as List? ?? [];
|
||||
final exercise = data['exercisePlan'] as Map<String, dynamic>?;
|
||||
|
||||
return ListView(padding: const EdgeInsets.all(16), children: [
|
||||
// 基本信息卡片
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(14)),
|
||||
child: Row(children: [
|
||||
CircleAvatar(radius: 28, backgroundColor: const Color(0xFFF0F0FF), child: Text((profile['name'] ?? '患')[0], style: const TextStyle(fontSize: 22, color: AppColors.primary))),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text(profile['name'] ?? '未设置', style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w700)),
|
||||
Text(profile['phone'] ?? '', style: const TextStyle(fontSize: 13, color: AppColors.textHint)),
|
||||
if (profile['gender'] != null) Text('${profile['gender']} · ${profile['birthDate'] ?? ''}', style: const TextStyle(fontSize: 12, color: AppColors.textHint)),
|
||||
])),
|
||||
]),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// 健康档案
|
||||
if (archive != null) ...[
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
// 基本信息卡片
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(14)),
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
const Text('健康档案', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600)),
|
||||
const SizedBox(height: 8),
|
||||
if (archive['diagnosis'] != null) _row('诊断', archive['diagnosis']),
|
||||
if (archive['surgeryType'] != null) _row('手术史', '${archive['surgeryType']} ${archive['surgeryDate'] ?? ''}'),
|
||||
if (archive['allergies'] != null && (archive['allergies'] as List).isNotEmpty) _row('过敏史', (archive['allergies'] as List).join('、')),
|
||||
if (archive['chronicDiseases'] != null && (archive['chronicDiseases'] as List).isNotEmpty) _row('慢病', (archive['chronicDiseases'] as List).join('、')),
|
||||
if (archive['familyHistory'] != null) _row('家族史', archive['familyHistory']),
|
||||
]),
|
||||
decoration: _detailCardDecoration(),
|
||||
child: Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 28,
|
||||
backgroundColor: AppColors.avatarBg,
|
||||
child: Text(
|
||||
(profile['name'] ?? '患')[0],
|
||||
style: const TextStyle(
|
||||
fontSize: 22,
|
||||
color: AppColors.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
profile['name'] ?? '未设置',
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
profile['phone'] ?? '',
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: AppColors.textHint,
|
||||
),
|
||||
),
|
||||
if (profile['gender'] != null)
|
||||
Text(
|
||||
'${profile['gender']} · ${profile['birthDate'] ?? ''}',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.textHint,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
|
||||
// 最新健康指标
|
||||
if (latest.isNotEmpty) ...[
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(14)),
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
const Text('健康指标', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600)),
|
||||
const SizedBox(height: 8),
|
||||
...latest.map((r) {
|
||||
final type = r['metricType']?.toString() ?? '';
|
||||
String val = '';
|
||||
if (type == 'BloodPressure') val = '${r['systolic'] ?? '--'}/${r['diastolic'] ?? '--'} mmHg';
|
||||
else val = '${r['value'] ?? '--'} ${r['unit'] ?? ''}';
|
||||
return Padding(padding: const EdgeInsets.only(bottom: 4), child: Row(children: [
|
||||
Text(_metricLabel(type), style: const TextStyle(fontSize: 14, color: AppColors.textHint)),
|
||||
const Spacer(),
|
||||
Text(val, style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: r['isAbnormal'] == true ? AppColors.error : AppColors.textPrimary)),
|
||||
]));
|
||||
}),
|
||||
]),
|
||||
// 健康档案
|
||||
if (archive != null) ...[
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: _detailCardDecoration(),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'健康档案',
|
||||
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (archive['diagnosis'] != null)
|
||||
_row('诊断', archive['diagnosis']),
|
||||
if (archive['surgeryType'] != null)
|
||||
_row(
|
||||
'手术史',
|
||||
'${archive['surgeryType']} ${archive['surgeryDate'] ?? ''}',
|
||||
),
|
||||
if (archive['allergies'] != null &&
|
||||
(archive['allergies'] as List).isNotEmpty)
|
||||
_row('过敏史', (archive['allergies'] as List).join('、')),
|
||||
if (archive['chronicDiseases'] != null &&
|
||||
(archive['chronicDiseases'] as List).isNotEmpty)
|
||||
_row('慢病', (archive['chronicDiseases'] as List).join('、')),
|
||||
if (archive['familyHistory'] != null)
|
||||
_row('家族史', archive['familyHistory']),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
|
||||
// 最新健康指标
|
||||
if (latest.isNotEmpty) ...[
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: _detailCardDecoration(),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'健康指标',
|
||||
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
...latest.map((r) {
|
||||
final type = r['metricType']?.toString() ?? '';
|
||||
String val = '';
|
||||
if (type == 'BloodPressure')
|
||||
val =
|
||||
'${r['systolic'] ?? '--'}/${r['diastolic'] ?? '--'} mmHg';
|
||||
else
|
||||
val = '${r['value'] ?? '--'} ${r['unit'] ?? ''}';
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
_metricLabel(type),
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.textHint,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
val,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: r['isAbnormal'] == true
|
||||
? AppColors.error
|
||||
: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
|
||||
// 用药列表
|
||||
if (meds.isNotEmpty)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: _detailCardDecoration(),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'当前用药',
|
||||
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
...meds.map(
|
||||
(m) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 4),
|
||||
child: Text(
|
||||
'${m['name']} ${m['dosage'] ?? ''} ${_freqLabel(m['frequency'] ?? '')}',
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 报告和随访入口
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _LinkCard(
|
||||
'报告 (${reports.length})',
|
||||
Icons.description_outlined,
|
||||
() {},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _LinkCard(
|
||||
'随访 (${followUps.length})',
|
||||
Icons.event_note_outlined,
|
||||
() {},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
|
||||
// 用药列表
|
||||
if (meds.isNotEmpty)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(14)),
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
const Text('当前用药', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600)),
|
||||
const SizedBox(height: 8),
|
||||
...meds.map((m) => Padding(padding: const EdgeInsets.only(bottom: 4), child: Text('${m['name']} ${m['dosage'] ?? ''} ${_freqLabel(m['frequency'] ?? '')}', style: const TextStyle(fontSize: 14)))),
|
||||
]),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 报告和随访入口
|
||||
Row(children: [
|
||||
Expanded(child: _LinkCard('报告 (${reports.length})', Icons.description_outlined, () {})),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: _LinkCard('随访 (${followUps.length})', Icons.event_note_outlined, () {})),
|
||||
]),
|
||||
]);
|
||||
);
|
||||
}
|
||||
|
||||
Widget _row(String label, String? value) => Padding(padding: const EdgeInsets.only(bottom: 4), child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
SizedBox(width: 56, child: Text(label, style: const TextStyle(fontSize: 13, color: AppColors.textHint))),
|
||||
Expanded(child: Text(value ?? '', style: const TextStyle(fontSize: 14))),
|
||||
]));
|
||||
Widget _row(String label, String? value) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 4),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 56,
|
||||
child: Text(
|
||||
label,
|
||||
style: const TextStyle(fontSize: 13, color: AppColors.textHint),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(value ?? '', style: const TextStyle(fontSize: 14)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
String _metricLabel(String t) => switch (t) { 'BloodPressure' => '血压', 'HeartRate' => '心率', 'Glucose' => '血糖', 'SpO2' => '血氧', 'Weight' => '体重', _ => t };
|
||||
String _freqLabel(String f) => switch (f) { 'Daily' => '每日', 'Weekly' => '每周', 'Monthly' => '每月', 'AsNeeded' => '按需', _ => f };
|
||||
BoxDecoration _detailCardDecoration() => BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: AppColors.border),
|
||||
boxShadow: AppColors.cardShadowLight,
|
||||
);
|
||||
|
||||
String _metricLabel(String t) => switch (t) {
|
||||
'BloodPressure' => '血压',
|
||||
'HeartRate' => '心率',
|
||||
'Glucose' => '血糖',
|
||||
'SpO2' => '血氧',
|
||||
'Weight' => '体重',
|
||||
_ => t,
|
||||
};
|
||||
String _freqLabel(String f) => switch (f) {
|
||||
'Daily' => '每日',
|
||||
'Weekly' => '每周',
|
||||
'Monthly' => '每月',
|
||||
'AsNeeded' => '按需',
|
||||
_ => f,
|
||||
};
|
||||
}
|
||||
|
||||
class _LinkCard extends StatelessWidget {
|
||||
@@ -136,16 +296,28 @@ class _LinkCard extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final VoidCallback onTap;
|
||||
const _LinkCard(this.label, this.icon, this.onTap);
|
||||
@override Widget build(BuildContext context) => GestureDetector(
|
||||
@override
|
||||
Widget build(BuildContext context) => GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(14)),
|
||||
child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||
Icon(icon, size: 18, color: AppColors.primary),
|
||||
const SizedBox(width: 6),
|
||||
Text(label, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),
|
||||
]),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: AppColors.border),
|
||||
boxShadow: AppColors.cardShadowLight,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(icon, size: 18, color: AppColors.primary),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@ import '../../providers/auth_provider.dart';
|
||||
|
||||
class DoctorPatientsPage extends ConsumerStatefulWidget {
|
||||
const DoctorPatientsPage({super.key});
|
||||
@override ConsumerState<DoctorPatientsPage> createState() => _DoctorPatientsPageState();
|
||||
@override
|
||||
ConsumerState<DoctorPatientsPage> createState() => _DoctorPatientsPageState();
|
||||
}
|
||||
|
||||
class _DoctorPatientsPageState extends ConsumerState<DoctorPatientsPage> {
|
||||
@@ -17,74 +18,144 @@ class _DoctorPatientsPageState extends ConsumerState<DoctorPatientsPage> {
|
||||
bool _hasMore = true;
|
||||
int _total = 0;
|
||||
|
||||
@override void initState() { super.initState(); _load(); }
|
||||
@override void dispose() { _searchCtrl.dispose(); super.dispose(); }
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_load();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchCtrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _load({bool reset = false}) async {
|
||||
if (_loading) return;
|
||||
if (reset) { _page = 1; _patients.clear(); }
|
||||
if (reset) {
|
||||
_page = 1;
|
||||
_patients.clear();
|
||||
}
|
||||
setState(() => _loading = true);
|
||||
try {
|
||||
final api = ref.read(apiClientProvider);
|
||||
final params = <String, dynamic>{'page': _page, 'pageSize': 20};
|
||||
final search = _searchCtrl.text.trim();
|
||||
if (search.isNotEmpty) params['search'] = search;
|
||||
final res = await api.get('/api/doctor/patients', queryParameters: params);
|
||||
final res = await api.get(
|
||||
'/api/doctor/patients',
|
||||
queryParameters: params,
|
||||
);
|
||||
final data = res.data['data'];
|
||||
if (data != null) {
|
||||
final items = (data['items'] as List?)?.cast<Map<String, dynamic>>() ?? [];
|
||||
final items =
|
||||
(data['items'] as List?)?.cast<Map<String, dynamic>>() ?? [];
|
||||
setState(() {
|
||||
if (reset) _patients = items; else _patients.addAll(items);
|
||||
if (reset)
|
||||
_patients = items;
|
||||
else
|
||||
_patients.addAll(items);
|
||||
_total = data['total'] ?? 0;
|
||||
_hasMore = _patients.length < _total;
|
||||
_page++;
|
||||
});
|
||||
}
|
||||
} catch (_) {} finally {
|
||||
} catch (_) {
|
||||
} finally {
|
||||
if (mounted) setState(() => _loading = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onSearch() async { await _load(reset: true); }
|
||||
Future<void> _loadMore() async { if (_hasMore) await _load(); }
|
||||
Future<void> _onSearch() async {
|
||||
await _load(reset: true);
|
||||
}
|
||||
|
||||
@override Widget build(BuildContext context) {
|
||||
return Column(children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: TextField(
|
||||
controller: _searchCtrl,
|
||||
onSubmitted: (_) => _onSearch(),
|
||||
decoration: InputDecoration(
|
||||
hintText: '搜索患者姓名或手机号',
|
||||
prefixIcon: const Icon(Icons.search, color: AppColors.textHint),
|
||||
suffixIcon: _searchCtrl.text.isNotEmpty ? IconButton(icon: const Icon(Icons.clear), onPressed: () { _searchCtrl.clear(); _onSearch(); }) : null,
|
||||
filled: true, fillColor: Colors.white,
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 12),
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none),
|
||||
Future<void> _loadMore() async {
|
||||
if (_hasMore) await _load();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: TextField(
|
||||
controller: _searchCtrl,
|
||||
onSubmitted: (_) => _onSearch(),
|
||||
decoration: InputDecoration(
|
||||
hintText: '搜索患者姓名或手机号',
|
||||
prefixIcon: const Icon(Icons.search, color: AppColors.textHint),
|
||||
suffixIcon: _searchCtrl.text.isNotEmpty
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
_searchCtrl.clear();
|
||||
_onSearch();
|
||||
},
|
||||
)
|
||||
: null,
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 12),
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () => _load(reset: true),
|
||||
child: _patients.isEmpty && !_loading
|
||||
? ListView(children: const [SizedBox(height: 100), Center(child: Text('暂无患者数据', style: TextStyle(color: AppColors.textHint)))])
|
||||
: ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
itemCount: _patients.length + (_hasMore ? 1 : 0),
|
||||
itemBuilder: (_, i) {
|
||||
if (i >= _patients.length) {
|
||||
_loadMore();
|
||||
return const Padding(padding: EdgeInsets.all(16), child: Center(child: CircularProgressIndicator(strokeWidth: 2)));
|
||||
}
|
||||
final p = _patients[i];
|
||||
return _PatientTile(p: p, onTap: () => pushRoute(ref, 'doctorPatientDetail', params: {'id': p['id']?.toString() ?? ''}));
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () => _load(reset: true),
|
||||
child: _patients.isEmpty && !_loading
|
||||
? ListView(
|
||||
children: const [
|
||||
SizedBox(height: 100),
|
||||
Center(
|
||||
child: Text(
|
||||
'暂无患者数据',
|
||||
style: TextStyle(color: AppColors.textHint),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
itemCount: _patients.length + (_hasMore ? 1 : 0),
|
||||
itemBuilder: (_, i) {
|
||||
if (i >= _patients.length) {
|
||||
_loadMore();
|
||||
return const Padding(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
final p = _patients[i];
|
||||
return _PatientTile(
|
||||
p: p,
|
||||
onTap: () => pushRoute(
|
||||
ref,
|
||||
'doctorPatientDetail',
|
||||
params: {'id': p['id']?.toString() ?? ''},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
]);
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,24 +164,58 @@ class _PatientTile extends StatelessWidget {
|
||||
final VoidCallback onTap;
|
||||
const _PatientTile({required this.p, required this.onTap});
|
||||
|
||||
@override Widget build(BuildContext context) {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final gender = p['gender'] ?? '';
|
||||
final genderIcon = gender == 'Male' ? Icons.male : gender == 'Female' ? Icons.female : Icons.person;
|
||||
final genderIcon = gender == 'Male'
|
||||
? Icons.male
|
||||
: gender == 'Female'
|
||||
? Icons.female
|
||||
: Icons.person;
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(14)),
|
||||
child: Row(children: [
|
||||
CircleAvatar(radius: 22, backgroundColor: const Color(0xFFF0F0FF), child: Icon(genderIcon, color: AppColors.primary)),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text(p['name'] ?? p['phone'] ?? '', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: AppColors.textPrimary)),
|
||||
Text(p['phone'] ?? '', style: const TextStyle(fontSize: 13, color: AppColors.textHint)),
|
||||
])),
|
||||
const Icon(Icons.chevron_right, color: AppColors.textHint),
|
||||
]),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: AppColors.border),
|
||||
boxShadow: AppColors.cardShadowLight,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 22,
|
||||
backgroundColor: AppColors.avatarBg,
|
||||
child: Icon(genderIcon, color: AppColors.primary),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
p['name'] ?? p['phone'] ?? '',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
p['phone'] ?? '',
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: AppColors.textHint,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Icon(Icons.chevron_right, color: AppColors.textHint),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,9 @@ final _docProfileProvider = FutureProvider<Map<String, dynamic>?>((ref) async {
|
||||
|
||||
class DoctorProfileEditPage extends ConsumerStatefulWidget {
|
||||
const DoctorProfileEditPage({super.key});
|
||||
@override ConsumerState<DoctorProfileEditPage> createState() => _DoctorProfileEditPageState();
|
||||
@override
|
||||
ConsumerState<DoctorProfileEditPage> createState() =>
|
||||
_DoctorProfileEditPageState();
|
||||
}
|
||||
|
||||
class _DoctorProfileEditPageState extends ConsumerState<DoctorProfileEditPage> {
|
||||
@@ -22,33 +24,73 @@ class _DoctorProfileEditPageState extends ConsumerState<DoctorProfileEditPage> {
|
||||
final _hosp = TextEditingController();
|
||||
bool _saving = false;
|
||||
|
||||
@override void dispose() { _name.dispose(); _title.dispose(); _dept.dispose(); _hosp.dispose(); super.dispose(); }
|
||||
@override
|
||||
void dispose() {
|
||||
_name.dispose();
|
||||
_title.dispose();
|
||||
_dept.dispose();
|
||||
_hosp.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override Widget build(BuildContext context) {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final prof = ref.watch(_docProfileProvider);
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF5F5F5),
|
||||
appBar: AppBar(backgroundColor: Colors.white, elevation: 0, title: const Text('个人信息', style: TextStyle(color: AppColors.textPrimary)), leading: IconButton(icon: const Icon(Icons.arrow_back, color: AppColors.textPrimary), onPressed: () => popRoute(ref))),
|
||||
backgroundColor: AppColors.background,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
title: const Text(
|
||||
'个人信息',
|
||||
style: TextStyle(color: AppColors.textPrimary),
|
||||
),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back, color: AppColors.textPrimary),
|
||||
onPressed: () => popRoute(ref),
|
||||
),
|
||||
),
|
||||
body: prof.when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (_, __) => const Center(child: Text('加载失败')),
|
||||
data: (d) {
|
||||
if (d != null) { _name.text = d['name'] ?? ''; _title.text = d['title'] ?? ''; _dept.text = d['department'] ?? ''; _hosp.text = d['hospital'] ?? ''; }
|
||||
return ListView(padding: const EdgeInsets.all(16), children: [
|
||||
_Field('姓名', _name),
|
||||
const SizedBox(height: 12),
|
||||
_Field('职称', _title),
|
||||
const SizedBox(height: 12),
|
||||
_Field('科室', _dept),
|
||||
const SizedBox(height: 12),
|
||||
_Field('医院', _hosp),
|
||||
const SizedBox(height: 24),
|
||||
SizedBox(width: double.infinity, child: ElevatedButton(
|
||||
onPressed: _saving ? null : _save,
|
||||
style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFF0891B2), foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14))),
|
||||
child: _saving ? const CircularProgressIndicator(color: Colors.white) : const Text('保存', style: TextStyle(fontSize: 16)),
|
||||
)),
|
||||
]);
|
||||
if (d != null) {
|
||||
_name.text = d['name'] ?? '';
|
||||
_title.text = d['title'] ?? '';
|
||||
_dept.text = d['department'] ?? '';
|
||||
_hosp.text = d['hospital'] ?? '';
|
||||
}
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
_Field('姓名', _name),
|
||||
const SizedBox(height: 12),
|
||||
_Field('职称', _title),
|
||||
const SizedBox(height: 12),
|
||||
_Field('科室', _dept),
|
||||
const SizedBox(height: 12),
|
||||
_Field('医院', _hosp),
|
||||
const SizedBox(height: 24),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: _saving ? null : _save,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primary,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
),
|
||||
child: _saving
|
||||
? const CircularProgressIndicator(color: Colors.white)
|
||||
: const Text('保存', style: TextStyle(fontSize: 16)),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -57,13 +99,33 @@ class _DoctorProfileEditPageState extends ConsumerState<DoctorProfileEditPage> {
|
||||
Future<void> _save() async {
|
||||
setState(() => _saving = true);
|
||||
try {
|
||||
await ref.read(apiClientProvider).put('/api/doctor/profile', data: {
|
||||
'name': _name.text, 'title': _title.text, 'department': _dept.text, 'hospital': _hosp.text,
|
||||
});
|
||||
await ref
|
||||
.read(apiClientProvider)
|
||||
.put(
|
||||
'/api/doctor/profile',
|
||||
data: {
|
||||
'name': _name.text,
|
||||
'title': _title.text,
|
||||
'department': _dept.text,
|
||||
'hospital': _hosp.text,
|
||||
},
|
||||
);
|
||||
ref.invalidate(_docProfileProvider);
|
||||
if (mounted) ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('保存成功'), backgroundColor: AppColors.success));
|
||||
if (mounted)
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('保存成功'),
|
||||
backgroundColor: AppColors.success,
|
||||
),
|
||||
);
|
||||
} catch (_) {
|
||||
if (mounted) ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('保存失败'), backgroundColor: AppColors.error));
|
||||
if (mounted)
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('保存失败'),
|
||||
backgroundColor: AppColors.error,
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
if (mounted) setState(() => _saving = false);
|
||||
}
|
||||
@@ -71,11 +133,26 @@ class _DoctorProfileEditPageState extends ConsumerState<DoctorProfileEditPage> {
|
||||
}
|
||||
|
||||
class _Field extends StatelessWidget {
|
||||
final String label; final TextEditingController ctrl;
|
||||
final String label;
|
||||
final TextEditingController ctrl;
|
||||
const _Field(this.label, this.ctrl);
|
||||
@override Widget build(BuildContext context) => Container(
|
||||
@override
|
||||
Widget build(BuildContext context) => Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(14)),
|
||||
child: TextField(controller: ctrl, decoration: InputDecoration(labelText: label, border: InputBorder.none)),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: AppColors.border),
|
||||
),
|
||||
child: TextField(
|
||||
controller: ctrl,
|
||||
decoration: InputDecoration(
|
||||
labelText: label,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
borderSide: const BorderSide(color: Colors.transparent),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,20 +5,24 @@ import '../../core/app_colors.dart';
|
||||
import '../../core/navigation_provider.dart';
|
||||
import '../../providers/auth_provider.dart';
|
||||
|
||||
final _reportDetailProvider = FutureProvider.family<Map<String, dynamic>?, String>((ref, id) async {
|
||||
final api = ref.read(apiClientProvider);
|
||||
final res = await api.get('/api/doctor/reports/$id');
|
||||
return res.data['data'] as Map<String, dynamic>?;
|
||||
});
|
||||
final _reportDetailProvider =
|
||||
FutureProvider.family<Map<String, dynamic>?, String>((ref, id) async {
|
||||
final api = ref.read(apiClientProvider);
|
||||
final res = await api.get('/api/doctor/reports/$id');
|
||||
return res.data['data'] as Map<String, dynamic>?;
|
||||
});
|
||||
|
||||
class DoctorReportDetailPage extends ConsumerStatefulWidget {
|
||||
final String id;
|
||||
const DoctorReportDetailPage({super.key, required this.id});
|
||||
|
||||
@override ConsumerState<DoctorReportDetailPage> createState() => _DoctorReportDetailPageState();
|
||||
@override
|
||||
ConsumerState<DoctorReportDetailPage> createState() =>
|
||||
_DoctorReportDetailPageState();
|
||||
}
|
||||
|
||||
class _DoctorReportDetailPageState extends ConsumerState<DoctorReportDetailPage> {
|
||||
class _DoctorReportDetailPageState
|
||||
extends ConsumerState<DoctorReportDetailPage> {
|
||||
String _severity = '';
|
||||
String _comment = '';
|
||||
String _recommendation = '';
|
||||
@@ -26,48 +30,90 @@ class _DoctorReportDetailPageState extends ConsumerState<DoctorReportDetailPage>
|
||||
bool _submitted = false;
|
||||
|
||||
static const _severityOptions = ['Normal', 'Abnormal', 'Severe', 'Critical'];
|
||||
static const _severityLabels = {'Normal': '正常', 'Abnormal': '异常', 'Severe': '严重', 'Critical': '危急'};
|
||||
static const _severityColors = {'Normal': 0xFF10B981, 'Abnormal': 0xFFF59E0B, 'Severe': 0xFFEF4444, 'Critical': 0xFF991B1B};
|
||||
static const _severityLabels = {
|
||||
'Normal': '正常',
|
||||
'Abnormal': '异常',
|
||||
'Severe': '严重',
|
||||
'Critical': '危急',
|
||||
};
|
||||
static const _severityColors = {
|
||||
'Normal': 0xFF10B981,
|
||||
'Abnormal': 0xFFF59E0B,
|
||||
'Severe': 0xFFEF4444,
|
||||
'Critical': 0xFF991B1B,
|
||||
};
|
||||
|
||||
static const _templates = ['药物剂量调整', '复查建议', '生活方式干预', '进一步检查', '专科转诊', '无需特殊处理'];
|
||||
static const _templates = [
|
||||
'药物剂量调整',
|
||||
'复查建议',
|
||||
'生活方式干预',
|
||||
'进一步检查',
|
||||
'专科转诊',
|
||||
'无需特殊处理',
|
||||
];
|
||||
|
||||
Future<void> _submitReview() async {
|
||||
if (_severity.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('请选择严重程度')));
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('请选择严重程度')));
|
||||
return;
|
||||
}
|
||||
setState(() => _submitting = true);
|
||||
try {
|
||||
final api = ref.read(apiClientProvider);
|
||||
await api.post('/api/doctor/reports/${widget.id}/review', data: {
|
||||
'severity': _severity,
|
||||
'comment': _comment,
|
||||
'recommendation': _recommendation,
|
||||
});
|
||||
await api.post(
|
||||
'/api/doctor/reports/${widget.id}/review',
|
||||
data: {
|
||||
'severity': _severity,
|
||||
'comment': _comment,
|
||||
'recommendation': _recommendation,
|
||||
},
|
||||
);
|
||||
setState(() => _submitted = true);
|
||||
ref.invalidate(_reportDetailProvider(widget.id));
|
||||
if (mounted) ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('审核提交成功'), backgroundColor: AppColors.success));
|
||||
if (mounted)
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('审核提交成功'),
|
||||
backgroundColor: AppColors.success,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('提交失败: $e'), backgroundColor: AppColors.error));
|
||||
if (mounted)
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('提交失败: $e'), backgroundColor: AppColors.error),
|
||||
);
|
||||
} finally {
|
||||
if (mounted) setState(() => _submitting = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override Widget build(BuildContext context) {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final detail = ref.watch(_reportDetailProvider(widget.id));
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF5F5F5),
|
||||
backgroundColor: AppColors.background,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.white, elevation: 0,
|
||||
title: const Text('报告审核', style: TextStyle(color: AppColors.textPrimary)),
|
||||
leading: IconButton(icon: const Icon(Icons.arrow_back, color: AppColors.textPrimary), onPressed: () => popRoute(ref)),
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
title: const Text(
|
||||
'报告审核',
|
||||
style: TextStyle(color: AppColors.textPrimary),
|
||||
),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back, color: AppColors.textPrimary),
|
||||
onPressed: () => popRoute(ref),
|
||||
),
|
||||
),
|
||||
body: detail.when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (_, __) => const Center(child: Text('加载失败')),
|
||||
data: (data) => data == null ? const Center(child: Text('报告不存在')) : _buildBody(data),
|
||||
data: (data) => data == null
|
||||
? const Center(child: Text('报告不存在'))
|
||||
: _buildBody(data),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -81,168 +127,427 @@ class _DoctorReportDetailPageState extends ConsumerState<DoctorReportDetailPage>
|
||||
final severity = data['severity'] as String?;
|
||||
final fileUrl = data['fileUrl'] as String?;
|
||||
|
||||
return ListView(padding: const EdgeInsets.all(16), children: [
|
||||
// 患者+分类信息
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(14)),
|
||||
child: Row(children: [
|
||||
CircleAvatar(radius: 24, backgroundColor: const Color(0xFFF0F0FF), child: Text((data['patientName'] ?? '?')[0], style: const TextStyle(color: AppColors.primary))),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text(data['patientName'] ?? '', style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 16)),
|
||||
Text('${data['category'] ?? ''} · ${data['fileType'] ?? ''}', style: const TextStyle(fontSize: 13, color: AppColors.textHint)),
|
||||
])),
|
||||
_StatusTag(data['status']?.toString() ?? ''),
|
||||
]),
|
||||
),
|
||||
|
||||
// AI预分析摘要
|
||||
if (aiSummary != null && aiSummary.isNotEmpty) ...[
|
||||
const SizedBox(height: 12),
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
// 患者+分类信息
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(14)),
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Row(children: [Container(width: 4, height: 16, decoration: BoxDecoration(color: const Color(0xFF6366F1), borderRadius: BorderRadius.circular(2))), const SizedBox(width: 8), const Text('AI 预分析', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600))]),
|
||||
const SizedBox(height: 8),
|
||||
Text(aiSummary, style: const TextStyle(fontSize: 14, color: AppColors.textSecondary, height: 1.5)),
|
||||
]),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: AppColors.border),
|
||||
boxShadow: AppColors.cardShadowLight,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 24,
|
||||
backgroundColor: AppColors.avatarBg,
|
||||
child: Text(
|
||||
(data['patientName'] ?? '?')[0],
|
||||
style: const TextStyle(color: AppColors.primary),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
data['patientName'] ?? '',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${data['category'] ?? ''} · ${data['fileType'] ?? ''}',
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: AppColors.textHint,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
_StatusTag(data['status']?.toString() ?? ''),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
// AI指标表格
|
||||
if (aiIndicators.isNotEmpty) ...[
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(14)),
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
const Text('指标检测', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600)),
|
||||
const SizedBox(height: 8),
|
||||
...aiIndicators.map((ind) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 6),
|
||||
child: Row(children: [
|
||||
Expanded(child: Text(ind['name'] ?? '', style: const TextStyle(fontSize: 14))),
|
||||
Text('${ind['value'] ?? ''}', style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600)),
|
||||
if (ind['unit'] != null) Text(' ${ind['unit']}', style: const TextStyle(fontSize: 12, color: AppColors.textHint)),
|
||||
const SizedBox(width: 8),
|
||||
if (ind['status'] != null)
|
||||
Container(padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration(color: _indicatorColor(ind['status']!).withOpacity(0.1), borderRadius: BorderRadius.circular(4)), child: Text(ind['status']!, style: TextStyle(fontSize: 11, color: _indicatorColor(ind['status']!)))),
|
||||
]),
|
||||
)),
|
||||
]),
|
||||
),
|
||||
],
|
||||
|
||||
// 查看原始报告
|
||||
if (fileUrl != null && fileUrl.isNotEmpty) ...[
|
||||
const SizedBox(height: 12),
|
||||
SizedBox(width: double.infinity, child: OutlinedButton.icon(
|
||||
onPressed: () {}, // TODO: 打开文件查看器
|
||||
icon: const Icon(Icons.visibility, size: 18),
|
||||
label: const Text('查看原始报告'),
|
||||
style: OutlinedButton.styleFrom(foregroundColor: AppColors.primary, side: const BorderSide(color: Color(0xFFE5E7EB)), padding: const EdgeInsets.symmetric(vertical: 12), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14))),
|
||||
)),
|
||||
],
|
||||
|
||||
// 医生审核区(未审核时显示)
|
||||
if (!reviewed && !_submitted) ...[
|
||||
const SizedBox(height: 20),
|
||||
Container(width: double.infinity, height: 1, color: const Color(0xFFE5E7EB)),
|
||||
const SizedBox(height: 16),
|
||||
const Text('医生审核', style: TextStyle(fontSize: 17, fontWeight: FontWeight.w700, color: AppColors.textPrimary)),
|
||||
const SizedBox(height: 14),
|
||||
|
||||
// 严重程度
|
||||
const Text('严重程度', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600)),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(spacing: 8, runSpacing: 8, children: _severityOptions.map((s) => GestureDetector(
|
||||
onTap: () => setState(() => _severity = s),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||
// AI预分析摘要
|
||||
if (aiSummary != null && aiSummary.isNotEmpty) ...[
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: _severity == s ? Color(_severityColors[s]!) : Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: _severity == s ? Color(_severityColors[s]!) : const Color(0xFFE5E7EB)),
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: AppColors.border),
|
||||
boxShadow: AppColors.cardShadowLight,
|
||||
),
|
||||
child: Text(_severityLabels[s]!, style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: _severity == s ? Colors.white : AppColors.textSecondary)),
|
||||
),
|
||||
)).toList()),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 建议模板
|
||||
const Text('建议模板(可多选)', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600)),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(spacing: 8, runSpacing: 8, children: _templates.map((t) {
|
||||
final selected = _recommendation.contains(t);
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
if (selected) {
|
||||
_recommendation = _recommendation.replaceAll('$t、', '').replaceAll('、$t', '').replaceAll(t, '');
|
||||
} else {
|
||||
_recommendation = _recommendation.isEmpty ? t : '$_recommendation、$t';
|
||||
}
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
||||
decoration: BoxDecoration(color: selected ? const Color(0xFFEFF6FF) : Colors.white, borderRadius: BorderRadius.circular(12), border: Border.all(color: selected ? const Color(0xFF0891B2) : const Color(0xFFE5E7EB))),
|
||||
child: Text(t, style: TextStyle(fontSize: 13, color: selected ? const Color(0xFF0891B2) : AppColors.textSecondary)),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 4,
|
||||
height: 16,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primary,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'AI 预分析',
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
aiSummary,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.textSecondary,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList()),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 自定义评语
|
||||
const Text('评语', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600)),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(14)),
|
||||
child: TextField(
|
||||
maxLines: 5,
|
||||
decoration: const InputDecoration(hintText: '输入审核评语(可选)', hintStyle: TextStyle(fontSize: 14, color: AppColors.textHint), border: InputBorder.none, contentPadding: EdgeInsets.all(16)),
|
||||
onChanged: (v) => _comment = v,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
const SizedBox(height: 20),
|
||||
SizedBox(width: double.infinity, child: ElevatedButton(
|
||||
onPressed: _submitting ? null : _submitReview,
|
||||
style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFF0891B2), foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14))),
|
||||
child: _submitting ? const CircularProgressIndicator(color: Colors.white) : const Text('提交审核', style: TextStyle(fontSize: 16)),
|
||||
)),
|
||||
],
|
||||
// AI指标表格
|
||||
if (aiIndicators.isNotEmpty) ...[
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'指标检测',
|
||||
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
...aiIndicators.map(
|
||||
(ind) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 6),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
ind['name'] ?? '',
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${ind['value'] ?? ''}',
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
if (ind['unit'] != null)
|
||||
Text(
|
||||
' ${ind['unit']}',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.textHint,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
if (ind['status'] != null)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: _indicatorColor(
|
||||
ind['status']!,
|
||||
).withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
ind['status']!,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: _indicatorColor(ind['status']!),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
// 已审核结果
|
||||
if (reviewed || _submitted) ...[
|
||||
const SizedBox(height: 20),
|
||||
Container(width: double.infinity, height: 1, color: const Color(0xFFE5E7EB)),
|
||||
const SizedBox(height: 16),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(14), border: Border.all(color: const Color(0xFFD1FAE5))),
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Row(children: [const Icon(Icons.check_circle, color: Color(0xFF10B981), size: 20), const SizedBox(width: 8), const Text('审核已完成', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: Color(0xFF10B981)))]),
|
||||
const SizedBox(height: 12),
|
||||
if (severity != null) _InfoRow('严重程度', _severityLabels[severity] ?? severity),
|
||||
if (doctorRec != null && doctorRec.isNotEmpty) _InfoRow('建议', doctorRec),
|
||||
if (doctorComment != null && doctorComment.isNotEmpty) _InfoRow('评语', doctorComment),
|
||||
]),
|
||||
),
|
||||
// 查看原始报告
|
||||
if (fileUrl != null && fileUrl.isNotEmpty) ...[
|
||||
const SizedBox(height: 12),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () {}, // TODO: 打开文件查看器
|
||||
icon: const Icon(Icons.visibility, size: 18),
|
||||
label: const Text('查看原始报告'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: AppColors.primary,
|
||||
side: const BorderSide(color: AppColors.border),
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
// 医生审核区(未审核时显示)
|
||||
if (!reviewed && !_submitted) ...[
|
||||
const SizedBox(height: 20),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: 1,
|
||||
color: AppColors.divider,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'医生审核',
|
||||
style: TextStyle(
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
|
||||
// 严重程度
|
||||
const Text(
|
||||
'严重程度',
|
||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: _severityOptions
|
||||
.map(
|
||||
(s) => GestureDetector(
|
||||
onTap: () => setState(() => _severity = s),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 10,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: _severity == s
|
||||
? Color(_severityColors[s]!)
|
||||
: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: _severity == s
|
||||
? Color(_severityColors[s]!)
|
||||
: AppColors.border,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
_severityLabels[s]!,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: _severity == s
|
||||
? Colors.white
|
||||
: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 建议模板
|
||||
const Text(
|
||||
'建议模板(可多选)',
|
||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: _templates.map((t) {
|
||||
final selected = _recommendation.contains(t);
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
if (selected) {
|
||||
_recommendation = _recommendation
|
||||
.replaceAll('$t、', '')
|
||||
.replaceAll('、$t', '')
|
||||
.replaceAll(t, '');
|
||||
} else {
|
||||
_recommendation = _recommendation.isEmpty
|
||||
? t
|
||||
: '$_recommendation、$t';
|
||||
}
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 14,
|
||||
vertical: 8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: selected ? AppColors.primarySoft : Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: selected ? AppColors.primary : AppColors.border,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
t,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: selected
|
||||
? AppColors.primary
|
||||
: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 自定义评语
|
||||
const Text(
|
||||
'评语',
|
||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: AppColors.border),
|
||||
boxShadow: AppColors.cardShadowLight,
|
||||
),
|
||||
child: TextField(
|
||||
maxLines: 5,
|
||||
decoration: InputDecoration(
|
||||
hintText: '输入审核评语(可选)',
|
||||
hintStyle: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.textHint,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
borderSide: const BorderSide(color: Colors.transparent),
|
||||
),
|
||||
contentPadding: const EdgeInsets.all(16),
|
||||
),
|
||||
onChanged: (v) => _comment = v,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: _submitting ? null : _submitReview,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primary,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
),
|
||||
child: _submitting
|
||||
? const CircularProgressIndicator(color: Colors.white)
|
||||
: const Text('提交审核', style: TextStyle(fontSize: 16)),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
// 已审核结果
|
||||
if (reviewed || _submitted) ...[
|
||||
const SizedBox(height: 20),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: 1,
|
||||
color: AppColors.divider,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: const Color(0xFFD1FAE5)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.check_circle,
|
||||
color: Color(0xFF10B981),
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'审核已完成',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Color(0xFF10B981),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
if (severity != null)
|
||||
_InfoRow('严重程度', _severityLabels[severity] ?? severity),
|
||||
if (doctorRec != null && doctorRec.isNotEmpty)
|
||||
_InfoRow('建议', doctorRec),
|
||||
if (doctorComment != null && doctorComment.isNotEmpty)
|
||||
_InfoRow('评语', doctorComment),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
const SizedBox(height: 24),
|
||||
]);
|
||||
);
|
||||
}
|
||||
|
||||
List<Map<String, String>> _parseIndicators(String? raw) {
|
||||
if (raw == null || raw.isEmpty) return [];
|
||||
try {
|
||||
final list = jsonDecode(raw);
|
||||
if (list is List) return list.map((e) => Map<String, String>.from(e as Map)).toList();
|
||||
if (list is List)
|
||||
return list.map((e) => Map<String, String>.from(e as Map)).toList();
|
||||
} catch (_) {}
|
||||
return [];
|
||||
}
|
||||
@@ -256,18 +561,39 @@ class _DoctorReportDetailPageState extends ConsumerState<DoctorReportDetailPage>
|
||||
|
||||
Widget _InfoRow(String label, String value) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 6),
|
||||
child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
SizedBox(width: 56, child: Text(label, style: const TextStyle(fontSize: 13, color: AppColors.textHint))),
|
||||
Expanded(child: Text(value, style: const TextStyle(fontSize: 14))),
|
||||
]),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 56,
|
||||
child: Text(
|
||||
label,
|
||||
style: const TextStyle(fontSize: 13, color: AppColors.textHint),
|
||||
),
|
||||
),
|
||||
Expanded(child: Text(value, style: const TextStyle(fontSize: 14))),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _StatusTag extends StatelessWidget {
|
||||
final String s;
|
||||
const _StatusTag(this.s);
|
||||
@override Widget build(BuildContext context) {
|
||||
final (label, color) = s == 'DoctorReviewed' ? ('已审核', const Color(0xFF10B981)) : s == 'PendingDoctor' ? ('待审核', const Color(0xFFF59E0B)) : (s, AppColors.textHint);
|
||||
return Container(padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration(color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(8)), child: Text(label, style: TextStyle(fontSize: 12, color: color)));
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final (label, color) = s == 'DoctorReviewed'
|
||||
? ('已审核', const Color(0xFF10B981))
|
||||
: s == 'PendingDoctor'
|
||||
? ('待审核', const Color(0xFFF59E0B))
|
||||
: (s, AppColors.textHint);
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(label, style: TextStyle(fontSize: 12, color: color)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,62 +4,127 @@ import '../../core/app_colors.dart';
|
||||
import '../../core/navigation_provider.dart';
|
||||
import '../../providers/auth_provider.dart';
|
||||
|
||||
final _reportsProvider = FutureProvider.family<List<Map<String, dynamic>>, String>((ref, status) async {
|
||||
final api = ref.read(apiClientProvider);
|
||||
final params = <String, dynamic>{};
|
||||
if (status.isNotEmpty && status != 'All') params['status'] = status;
|
||||
final res = await api.get('/api/doctor/reports', queryParameters: params.isNotEmpty ? params : null);
|
||||
return (res.data['data'] as List?)?.cast<Map<String, dynamic>>() ?? [];
|
||||
});
|
||||
final _reportsProvider =
|
||||
FutureProvider.family<List<Map<String, dynamic>>, String>((
|
||||
ref,
|
||||
status,
|
||||
) async {
|
||||
final api = ref.read(apiClientProvider);
|
||||
final params = <String, dynamic>{};
|
||||
if (status.isNotEmpty && status != 'All') params['status'] = status;
|
||||
final res = await api.get(
|
||||
'/api/doctor/reports',
|
||||
queryParameters: params.isNotEmpty ? params : null,
|
||||
);
|
||||
return (res.data['data'] as List?)?.cast<Map<String, dynamic>>() ?? [];
|
||||
});
|
||||
|
||||
class DoctorReportsPage extends ConsumerStatefulWidget {
|
||||
const DoctorReportsPage({super.key});
|
||||
@override ConsumerState<DoctorReportsPage> createState() => _DoctorReportsPageState();
|
||||
@override
|
||||
ConsumerState<DoctorReportsPage> createState() => _DoctorReportsPageState();
|
||||
}
|
||||
|
||||
class _DoctorReportsPageState extends ConsumerState<DoctorReportsPage> {
|
||||
String _filter = '';
|
||||
|
||||
@override Widget build(BuildContext context) {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final reports = ref.watch(_reportsProvider(_filter));
|
||||
return Column(children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(children: [
|
||||
_FilterChip('全部', '', _filter, () => setState(() => _filter = '')),
|
||||
const SizedBox(width: 8),
|
||||
_FilterChip('待审核', 'PendingDoctor', _filter, () => setState(() => _filter = 'PendingDoctor')),
|
||||
const SizedBox(width: 8),
|
||||
_FilterChip('已审核', 'DoctorReviewed', _filter, () => setState(() => _filter = 'DoctorReviewed')),
|
||||
]),
|
||||
),
|
||||
Expanded(child: reports.when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (_, __) => const Center(child: Text('加载失败')),
|
||||
data: (items) => items.isEmpty
|
||||
? const Center(child: Text('暂无报告', style: TextStyle(color: AppColors.textHint)))
|
||||
: ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
itemCount: items.length,
|
||||
itemBuilder: (_, i) {
|
||||
final r = items[i];
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(14)),
|
||||
child: ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: const Icon(Icons.description, color: AppColors.primary),
|
||||
title: Text(r['patientName'] ?? '', style: const TextStyle(fontWeight: FontWeight.w600)),
|
||||
subtitle: Text('${r['category'] ?? ''} · ${r['createdAt'] ?? ''}'.replaceAll('T', ' ').substring(0, 16)),
|
||||
trailing: const Icon(Icons.chevron_right, color: AppColors.textHint),
|
||||
onTap: () => pushRoute(ref, 'doctorReportDetail', params: {'id': r['id']?.toString() ?? ''}),
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
_FilterChip(
|
||||
'全部',
|
||||
'',
|
||||
_filter,
|
||||
() => setState(() => _filter = ''),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_FilterChip(
|
||||
'待审核',
|
||||
'PendingDoctor',
|
||||
_filter,
|
||||
() => setState(() => _filter = 'PendingDoctor'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_FilterChip(
|
||||
'已审核',
|
||||
'DoctorReviewed',
|
||||
_filter,
|
||||
() => setState(() => _filter = 'DoctorReviewed'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: reports.when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (_, __) => const Center(child: Text('加载失败')),
|
||||
data: (items) => items.isEmpty
|
||||
? const Center(
|
||||
child: Text(
|
||||
'暂无报告',
|
||||
style: TextStyle(color: AppColors.textHint),
|
||||
),
|
||||
)
|
||||
: ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
itemCount: items.length,
|
||||
itemBuilder: (_, i) {
|
||||
final r = items[i];
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: AppColors.border),
|
||||
boxShadow: AppColors.cardShadowLight,
|
||||
),
|
||||
child: ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.iconBg,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.description_outlined,
|
||||
color: AppColors.primary,
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
r['patientName'] ?? '',
|
||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
subtitle: Text(
|
||||
'${r['category'] ?? ''} · ${r['createdAt'] ?? ''}'
|
||||
.replaceAll('T', ' ')
|
||||
.substring(0, 16),
|
||||
),
|
||||
trailing: const Icon(
|
||||
Icons.chevron_right,
|
||||
color: AppColors.textHint,
|
||||
),
|
||||
onTap: () => pushRoute(
|
||||
ref,
|
||||
'doctorReportDetail',
|
||||
params: {'id': r['id']?.toString() ?? ''},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
)),
|
||||
]);
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,14 +132,30 @@ class _FilterChip extends StatelessWidget {
|
||||
final String label, value, current;
|
||||
final VoidCallback onTap;
|
||||
const _FilterChip(this.label, this.value, this.current, this.onTap);
|
||||
@override Widget build(BuildContext context) {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final active = value == current;
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 7),
|
||||
decoration: BoxDecoration(color: active ? AppColors.primary : Colors.white, borderRadius: BorderRadius.circular(20), border: Border.all(color: active ? AppColors.primary : const Color(0xFFE5E7EB))),
|
||||
child: Text(label, style: TextStyle(fontSize: 13, color: active ? Colors.white : AppColors.textSecondary)),
|
||||
decoration: BoxDecoration(
|
||||
color: active ? AppColors.primary : Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(
|
||||
color: active ? AppColors.primary : AppColors.border,
|
||||
),
|
||||
boxShadow: active
|
||||
? AppColors.buttonShadow
|
||||
: AppColors.cardShadowLight,
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: active ? Colors.white : AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,53 +6,129 @@ import '../../providers/auth_provider.dart';
|
||||
|
||||
class DoctorSettingsPage extends ConsumerWidget {
|
||||
const DoctorSettingsPage({super.key});
|
||||
@override Widget build(BuildContext context, WidgetRef ref) {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF5F5F5),
|
||||
appBar: AppBar(backgroundColor: Colors.white, elevation: 0, title: const Text('设置', style: TextStyle(color: AppColors.textPrimary)), leading: IconButton(icon: const Icon(Icons.arrow_back, color: AppColors.textPrimary), onPressed: () => popRoute(ref))),
|
||||
body: ListView(padding: const EdgeInsets.all(16), children: [
|
||||
_SettingTile(Icons.notifications_outlined, '推送通知', trailing: Switch(value: true, onChanged: (_) {})),
|
||||
_SettingTile(Icons.description_outlined, '隐私政策', onTap: () {}),
|
||||
_SettingTile(Icons.article_outlined, '用户协议', onTap: () {}),
|
||||
const SizedBox(height: 24),
|
||||
_SettingTile(Icons.delete_forever_outlined, '删除账号', color: Colors.red, onTap: () => _confirmDelete(context, ref)),
|
||||
const SizedBox(height: 24),
|
||||
SizedBox(width: double.infinity, child: ElevatedButton(
|
||||
onPressed: () async { await ref.read(authProvider.notifier).logout(); goRoute(ref, 'login'); },
|
||||
style: ElevatedButton.styleFrom(backgroundColor: Colors.white, foregroundColor: AppColors.error, elevation: 0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14), side: const BorderSide(color: Color(0xFFFECACA))), padding: const EdgeInsets.symmetric(vertical: 14)),
|
||||
child: const Text('退出登录'),
|
||||
)),
|
||||
]),
|
||||
backgroundColor: AppColors.background,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
title: const Text('设置', style: TextStyle(color: AppColors.textPrimary)),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back, color: AppColors.textPrimary),
|
||||
onPressed: () => popRoute(ref),
|
||||
),
|
||||
),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
_SettingTile(
|
||||
Icons.notifications_outlined,
|
||||
'推送通知',
|
||||
trailing: Switch(value: true, onChanged: (_) {}),
|
||||
),
|
||||
_SettingTile(Icons.description_outlined, '隐私政策', onTap: () {}),
|
||||
_SettingTile(Icons.article_outlined, '用户协议', onTap: () {}),
|
||||
const SizedBox(height: 24),
|
||||
_SettingTile(
|
||||
Icons.delete_forever_outlined,
|
||||
'删除账号',
|
||||
color: Colors.red,
|
||||
onTap: () => _confirmDelete(context, ref),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: () async {
|
||||
await ref.read(authProvider.notifier).logout();
|
||||
goRoute(ref, 'login');
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: AppColors.error,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
side: const BorderSide(color: Color(0xFFFECACA)),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
),
|
||||
child: const Text('退出登录'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _confirmDelete(BuildContext context, WidgetRef ref) {
|
||||
showDialog(context: context, builder: (ctx) => AlertDialog(
|
||||
title: const Text('删除账号'),
|
||||
content: const Text('此操作将永久删除你的所有数据,包括个人信息、问诊记录等。此操作不可撤销。'),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('取消')),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
Navigator.pop(ctx);
|
||||
try {
|
||||
await ref.read(apiClientProvider).delete('/api/user/account');
|
||||
await ref.read(authProvider.notifier).logout();
|
||||
if (context.mounted) goRoute(ref, 'login');
|
||||
} catch (_) {}
|
||||
},
|
||||
child: const Text('确认删除', style: TextStyle(color: Colors.red)),
|
||||
),
|
||||
],
|
||||
));
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('删除账号'),
|
||||
content: const Text('此操作将永久删除你的所有数据,包括个人信息、问诊记录等。此操作不可撤销。'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
Navigator.pop(ctx);
|
||||
try {
|
||||
await ref.read(apiClientProvider).delete('/api/user/account');
|
||||
await ref.read(authProvider.notifier).logout();
|
||||
if (context.mounted) goRoute(ref, 'login');
|
||||
} catch (_) {}
|
||||
},
|
||||
child: const Text('确认删除', style: TextStyle(color: Colors.red)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SettingTile extends StatelessWidget {
|
||||
final IconData icon; final String label; final Color? color; final Widget? trailing; final VoidCallback? onTap;
|
||||
const _SettingTile(this.icon, this.label, {this.color, this.trailing, this.onTap});
|
||||
@override Widget build(BuildContext context) => Container(
|
||||
margin: const EdgeInsets.only(bottom: 8), decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(14)),
|
||||
child: ListTile(leading: Icon(icon, color: color ?? AppColors.textPrimary), title: Text(label, style: TextStyle(fontSize: 15, color: color ?? AppColors.textPrimary)), trailing: trailing ?? (onTap != null ? const Icon(Icons.chevron_right, color: AppColors.textHint) : null), onTap: onTap),
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final Color? color;
|
||||
final Widget? trailing;
|
||||
final VoidCallback? onTap;
|
||||
const _SettingTile(
|
||||
this.icon,
|
||||
this.label, {
|
||||
this.color,
|
||||
this.trailing,
|
||||
this.onTap,
|
||||
});
|
||||
@override
|
||||
Widget build(BuildContext context) => Container(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: AppColors.border),
|
||||
boxShadow: AppColors.cardShadowLight,
|
||||
),
|
||||
child: ListTile(
|
||||
leading: Icon(icon, color: color ?? AppColors.primary),
|
||||
title: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
color: color ?? AppColors.textPrimary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
trailing:
|
||||
trailing ??
|
||||
(onTap != null
|
||||
? const Icon(Icons.chevron_right, color: AppColors.textHint)
|
||||
: null),
|
||||
onTap: onTap,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user