feat: AI 提示词模块化 + AI 同意门控 + AI 草稿存储 + 意图路由 + 医疗引用知识库 + 多页面 UI 优化

- AI 提示词拆分为 markdown 模块(Prompts/global + modules + rag + router)
- 新增 AI 同意门控(用户需同意后才能使用 AI 功能)
- 新增 AI 录入草稿存储(AiEntryDraftContracts/EfAiEntryDraftStore/AiEntryDraftRecord)
- 新增 AI 意图路由(ai_intent_router)
- 新增医疗引用知识库(MedicalCitationKnowledge)
- 重构 prompt_manager 和 ai_chat_endpoints
- 优化聊天/趋势/档案/抽屉/医生端等多个页面 UI
- 新增 agent 插画和趋势指标图标资源
- 删除 HANDOFF-2026-07-17.md
This commit is contained in:
MingNian
2026-07-27 16:53:20 +08:00
parent ad93e38b7e
commit 3b5cec10a6
80 changed files with 4358 additions and 1339 deletions

View File

@@ -1,4 +1,5 @@
import 'dart:io';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
@@ -11,6 +12,7 @@ import '../providers/auth_provider.dart';
import '../providers/chat_provider.dart';
import '../providers/conversation_history_provider.dart';
import 'app_toast.dart';
import 'authenticated_network_image.dart';
import '../providers/data_providers.dart';
import 'drawer_shell.dart';
@@ -137,9 +139,7 @@ class _AccountHeader extends StatelessWidget {
width: 66,
height: 66,
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
boxShadow: [
BoxShadow(
color: const Color(0xFF7C5CFF).withValues(alpha: 0.16),
@@ -148,10 +148,33 @@ class _AccountHeader extends StatelessWidget {
),
],
),
child: const Icon(
Icons.person_rounded,
color: Color(0xFF94A3B8),
size: 34,
child: Stack(
fit: StackFit.expand,
children: [
ClipOval(
clipBehavior: Clip.antiAlias,
child: ColoredBox(
color: const Color(0xFFF1F5F9),
child: user?.avatarUrl?.toString().isNotEmpty == true
? AuthenticatedNetworkImage(
imageUrl: user.avatarUrl.toString(),
fit: BoxFit.cover,
width: 66,
height: 66,
errorBuilder: (_, _, _) => const Icon(
Icons.person_rounded,
color: Color(0xFF94A3B8),
size: 34,
),
)
: const Icon(
Icons.person_rounded,
color: Color(0xFF94A3B8),
size: 34,
),
),
),
],
),
),
),
@@ -209,11 +232,6 @@ class _HealthDashboard extends StatelessWidget {
Widget build(BuildContext context) {
return _Panel(
title: '健康仪表盘',
trailing: _TextAction(
label: '详情',
light: true,
onTap: () => pushRoute(ref, 'trend'),
),
titleColor: Colors.white,
backgroundPainter: const _HealthDashboardBackgroundPainter(),
child: latestHealth.when(
@@ -310,55 +328,82 @@ class _MetricTile extends StatelessWidget {
@override
Widget build(BuildContext context) {
final elderMode = ElderModeScope.enabledOf(context);
return InkWell(
onTap: onTap,
final height = elderMode ? 112.0 : 100.0;
return ClipRRect(
borderRadius: BorderRadius.circular(18),
child: Container(
height: elderMode ? 112 : 100,
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 10),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.22),
borderRadius: BorderRadius.circular(18),
border: Border.all(color: Colors.white.withValues(alpha: 0.40)),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
height: 32,
child: FittedBox(
fit: BoxFit.scaleDown,
child: Text(
info.value,
maxLines: 1,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 26,
fontWeight: FontWeight.w700,
color: Colors.white,
child: BackdropFilter(
filter: ui.ImageFilter.blur(sigmaX: 10, sigmaY: 10),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
child: Container(
height: height,
padding: const EdgeInsets.symmetric(horizontal: 2, vertical: 7),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Colors.white.withValues(alpha: 0.62),
Colors.white.withValues(alpha: 0.42),
const Color(0xFFD9FCFF).withValues(alpha: 0.34),
],
),
borderRadius: BorderRadius.circular(18),
border: Border.all(
color: Colors.white.withValues(alpha: 0.82),
width: 1.2,
),
boxShadow: [
BoxShadow(
color: const Color(0xFF0369A1).withValues(alpha: 0.10),
blurRadius: 10,
offset: const Offset(0, 4),
),
),
],
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
height: 32,
child: FittedBox(
fit: BoxFit.scaleDown,
child: Text(
info.value,
maxLines: 1,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 23,
fontWeight: FontWeight.w800,
color: Colors.white,
),
),
),
),
if (info.unit != null)
Text(
info.unit!,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: Color(0xE8FFFFFF),
),
),
const SizedBox(height: 2),
Text(
info.label,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
color: Colors.white,
),
),
],
),
),
if (info.unit != null)
Text(
info.unit!,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xE0FFFFFF),
),
),
const SizedBox(height: 2),
Text(
info.label,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
],
),
),
),
);
@@ -372,7 +417,7 @@ class _NavigationSection extends StatelessWidget {
@override
Widget build(BuildContext context) {
final elderMode = ElderModeScope.enabledOf(context);
final allItems = <_NavItem>[
final items = [
_NavItem(
icon: LucideIcons.folderHeart,
title: '档案',
@@ -422,9 +467,6 @@ class _NavigationSection extends StatelessWidget {
colors: AppColors.deviceGradient.colors,
),
];
final items = Platform.isIOS
? allItems.where((item) => item.route != 'devices').toList()
: allItems;
return _LightSection(
title: '常用功能',
@@ -631,13 +673,11 @@ class _LightSection extends StatelessWidget {
class _Panel extends StatelessWidget {
final String title;
final Widget child;
final Widget? trailing;
final CustomPainter? backgroundPainter;
final Color titleColor;
const _Panel({
required this.title,
required this.child,
this.trailing,
this.backgroundPainter,
this.titleColor = AppColors.textPrimary,
});
@@ -680,7 +720,6 @@ class _Panel extends StatelessWidget {
),
),
),
?trailing,
],
),
const SizedBox(height: 12),
@@ -703,54 +742,49 @@ class _HealthDashboardBackgroundPainter extends CustomPainter {
..shader = const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xFF00C6FB), Color(0xFF005BEA)],
colors: [Color(0xFF4FACFE), Color(0xFF00F2FE)],
).createShader(rect);
canvas.drawRect(rect, paint);
final glow = Paint()
..shader =
RadialGradient(
colors: [
Colors.white.withValues(alpha: 0.32),
Colors.white.withValues(alpha: 0),
],
).createShader(
Rect.fromCircle(
center: Offset(size.width * 0.12, size.height * 0.05),
radius: size.width * 0.7,
),
);
canvas.drawCircle(
Offset(size.width * 0.12, size.height * 0.05),
size.width * 0.7,
glow,
);
final highlight = Paint()
..color = Colors.white.withValues(alpha: 0.20)
..style = PaintingStyle.stroke
..strokeWidth = 1.2;
canvas.drawLine(
Offset(size.width * 0.52, -12),
Offset(size.width * 0.16, size.height + 18),
highlight,
);
canvas.drawLine(
Offset(size.width * 0.96, -10),
Offset(size.width * 0.58, size.height + 22),
highlight,
);
}
@override
bool shouldRepaint(_HealthDashboardBackgroundPainter oldDelegate) => false;
}
class _TextAction extends StatelessWidget {
final String label;
final VoidCallback onTap;
final bool light;
const _TextAction({
required this.label,
required this.onTap,
this.light = false,
});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(999),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 11, vertical: 6),
decoration: BoxDecoration(
color: light
? Colors.white.withValues(alpha: 0.24)
: const Color(0xFFEDE9FE),
borderRadius: BorderRadius.circular(999),
border: light
? Border.all(color: Colors.white.withValues(alpha: 0.34))
: null,
),
child: Text(
label,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w700,
color: light ? Colors.white : Color(0xFF6D28D9),
),
),
),
);
}
}
class _IconButton extends StatelessWidget {
final IconData icon;
final VoidCallback onTap;