feat: 三端抽屉重构 + 配色系统更新 + 后台管理页精调 + 键盘抬起组件

## 抽屉重构
- admin_drawer / doctor_drawer / health_drawer 三端抽屉全部重做
- drawer_shell 增强

## 配色系统
- app_colors / app_module_visuals / app_theme 更新
- app.dart 启动流程调整

## 后台管理页
- admin_doctors_page 大幅重构(+251)
- admin_home / doctor_dashboard / doctor_reports / doctor_home / doctor_followups / doctor_settings 精调

## 患者端
- home_page / chat_messages_view / medication_list / notification_center / remaining_pages / exercise_plan / device / diet / consultation 微调

## 新增
- keyboard_lift.dart: 键盘抬起处理组件
- AGENTS.md: agent 指引文档

## 其他
- api_client IP 适配
- app_future_view 增强
- data_providers 调整
- secondary_page_visuals_test 更新
This commit is contained in:
MingNian
2026-07-19 19:11:30 +08:00
parent ae94ced2d5
commit 0d4fd88ce7
35 changed files with 1022 additions and 644 deletions

View File

@@ -53,6 +53,7 @@ class _DietRecordListPageState extends ConsumerState<DietRecordListPage> {
setState(() => _data.removeAt(index));
try {
await ref.read(dietServiceProvider).deleteRecord(id);
ref.invalidate(dietRecordsProvider);
if (mounted) {
AppToast.show(context, '已删除', type: AppToastType.success);
}
@@ -205,59 +206,66 @@ class _DietRecordListPageState extends ConsumerState<DietRecordListPage> {
padding: EdgeInsets.only(right: i == dates.length - 1 ? 0 : 5),
child: GestureDetector(
onTap: () => setState(() => _selectedDate = d),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 8),
decoration: BoxDecoration(
color: isSel
? AppColors.primary
: (isToday ? DietPalette.primarySoft : Colors.white),
borderRadius: AppRadius.mdBorder,
border: Border.all(
child: CustomPaint(
foregroundPainter: cal > 0
? const _DietDateGradientBorderPainter()
: null,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 8),
decoration: BoxDecoration(
color: isSel
? Colors.transparent
: (isToday
? const Color(0xFFC9D0FF)
: AppColors.borderLight),
? AppColors.primary
: (isToday ? DietPalette.primarySoft : Colors.white),
borderRadius: AppRadius.mdBorder,
border: cal > 0
? null
: Border.all(
color: isSel
? Colors.transparent
: (isToday
? const Color(0xFFC9D0FF)
: AppColors.borderLight),
),
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
FittedBox(
fit: BoxFit.scaleDown,
child: Text(
isToday
? '今天'
: ['', '', '', '', '', '', ''][d.weekday -
1],
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w700,
color: isSel ? Colors.white : AppColors.textHint,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
FittedBox(
fit: BoxFit.scaleDown,
child: Text(
isToday
? '今天'
: ['', '', '', '', '', '', ''][d.weekday -
1],
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w700,
color: isSel ? Colors.white : AppColors.textHint,
),
),
),
),
const SizedBox(height: 3),
Text(
'${d.day}',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
color: isSel ? Colors.white : AppColors.textPrimary,
),
),
if (cal > 0) ...[
const SizedBox(height: 2),
const SizedBox(height: 3),
Text(
'$cal',
'${d.day}',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: isSel ? Colors.white70 : AppColors.textHint,
fontSize: 18,
fontWeight: FontWeight.w700,
color: isSel ? Colors.white : AppColors.textPrimary,
),
),
if (cal > 0) ...[
const SizedBox(height: 2),
Text(
'$cal',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: isSel ? Colors.white70 : AppColors.primary,
),
),
],
],
],
),
),
),
),
@@ -557,7 +565,7 @@ class _DietTrendPanel extends StatelessWidget {
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary,
color: AppColors.primary,
),
),
const Spacer(),
@@ -633,14 +641,15 @@ class _TrendChart extends StatelessWidget {
'${data[i].toInt()}',
style: const TextStyle(
fontSize: 8,
color: AppColors.textHint,
color: AppColors.primary,
),
),
const SizedBox(height: 2),
Container(
height: h,
decoration: BoxDecoration(
color: data[i] > 0 ? DietPalette.primary : AppColors.border,
color: data[i] > 0 ? null : AppColors.border,
gradient: data[i] > 0 ? AppColors.primaryGradient : null,
borderRadius: BorderRadius.circular(2),
),
),
@@ -653,6 +662,32 @@ class _TrendChart extends StatelessWidget {
}
}
class _DietDateGradientBorderPainter extends CustomPainter {
const _DietDateGradientBorderPainter();
static const _strokeWidth = 1.5;
@override
void paint(Canvas canvas, Size size) {
final rect = (Offset.zero & size).deflate(_strokeWidth / 2);
final paint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = _strokeWidth
..shader = AppColors.primaryGradient.createShader(rect)
..isAntiAlias = true;
canvas.drawRRect(
RRect.fromRectAndRadius(
rect,
const Radius.circular(AppTheme.rMd - _strokeWidth / 2),
),
paint,
);
}
@override
bool shouldRepaint(_DietDateGradientBorderPainter oldDelegate) => false;
}
class _SwipeAction extends StatelessWidget {
final Key itemKey;
final Widget child;
@@ -709,19 +744,6 @@ class DietRecordDetailPage extends ConsumerStatefulWidget {
}
class _DietRecordDetailPageState extends ConsumerState<DietRecordDetailPage> {
late Future<List<Map<String, dynamic>>> _recordsFuture;
@override
void initState() {
super.initState();
_recordsFuture = _loadRecords();
}
Future<List<Map<String, dynamic>>> _loadRecords() =>
ref.read(dietServiceProvider).getRecords();
void _reload() => setState(() => _recordsFuture = _loadRecords());
String _formatRecordedAt(Object? value) {
final date = DateTime.tryParse(value?.toString() ?? '')?.toLocal();
if (date == null) return '';
@@ -742,22 +764,12 @@ class _DietRecordDetailPageState extends ConsumerState<DietRecordDetailPage> {
),
title: const Text('饮食详情'),
),
body: FutureBuilder<List<Map<String, dynamic>>>(
future: _recordsFuture,
builder: (ctx, snap) {
if (!snap.hasData) {
if (snap.hasError) {
return AppErrorState(
title: '饮食记录加载失败',
subtitle: '请检查网络后重新进入',
onRetry: _reload,
);
}
return const Center(
child: CircularProgressIndicator(color: AppTheme.primary),
);
}
final d = snap.data!.firstWhere(
body: AppAsyncValueView<List<Map<String, dynamic>>>(
value: ref.watch(dietRecordsProvider),
errorTitle: '饮食记录加载失败',
onRetry: () => ref.invalidate(dietRecordsProvider),
onData: (ctx, records) {
final d = records.firstWhere(
(r) => r['id']?.toString() == widget.id,
orElse: () => <String, dynamic>{},
);
@@ -2370,18 +2382,6 @@ class FollowUpListPage extends ConsumerStatefulWidget {
}
class _FollowUpListPageState extends ConsumerState<FollowUpListPage> {
Future<List<Map<String, dynamic>>>? _future;
@override
void initState() {
super.initState();
_load();
}
void _load() => setState(() {
_future = ref.read(followUpServiceProvider).getList();
});
@override
Widget build(BuildContext context) {
return GradientScaffold(
@@ -2393,18 +2393,11 @@ class _FollowUpListPageState extends ConsumerState<FollowUpListPage> {
title: const Text('复查随访'),
centerTitle: true,
),
body: FutureBuilder<List<Map<String, dynamic>>>(
future: _future,
builder: (ctx, snap) {
if (snap.connectionState == ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(color: AppTheme.primaryLight),
);
}
if (snap.hasError) {
return AppErrorState(title: '随访加载失败', onRetry: _load);
}
final list = snap.data ?? [];
body: AppAsyncValueView<List<Map<String, dynamic>>>(
value: ref.watch(followUpListProvider),
errorTitle: '随访加载失败',
onRetry: () => ref.invalidate(followUpListProvider),
onData: (ctx, list) {
if (list.isEmpty) {
return Center(
child: Column(