feat: UI 系统中心化 + 趋势图重构 + 法律文档 H5 上线

- UI 系统: 新增 design_tokens / module_visuals / app_buttons / app_status_badge / app_toast / ai_content 六个共享模块; 28 个页面接入, SnackBar 全部替换为 AppToast
- 趋势图: 直线折线 + 渐变填充 + 阴影; 去网格线; X 轴日+月份分隔; Y 轴整数刻度最多 4 个; 点击数据点/录入记录显示上方浮层, 选中点变实心
- 法律文档 H5: 后端 wwwroot 托管隐私政策/服务协议/关于/个人信息收集清单/第三方 SDK 清单 + 索引页; Program.cs 启用 UseDefaultFiles + UseStaticFiles
- 其他: auth_provider 登录流程调整; api_client IP 适配; 主页智能体栏间距优化
This commit is contained in:
MingNian
2026-07-08 21:25:07 +08:00
parent 7a93237069
commit 1c020b8ae5
45 changed files with 3480 additions and 1000 deletions

View File

@@ -1,14 +1,17 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import '../../../core/app_colors.dart';
import '../../../core/app_design_tokens.dart';
import '../../../core/app_module_visuals.dart';
import '../../../core/app_theme.dart';
import '../../../core/api_client.dart' show baseUrl;
import '../../../core/navigation_provider.dart';
import '../../../providers/chat_provider.dart';
import '../../../providers/data_providers.dart';
import '../../../widgets/ai_content.dart';
import '../../../widgets/app_toast.dart';
/// 卡片入场动画包装(从下往上滑入 + 淡入)
class _AnimatedCardEntry extends StatefulWidget {
@@ -260,20 +263,15 @@ class ChatMessagesView extends ConsumerWidget {
const SizedBox(height: 9),
Text(
info.$2,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.w800,
color: AppColors.textPrimary,
),
style: AppTextStyles.chatTitle.copyWith(
fontSize: 22,
fontWeight: FontWeight.w900,
),
),
const SizedBox(height: 8),
Text(
info.$3,
style: const TextStyle(
fontSize: 16,
color: AppColors.textSecondary,
height: 1.4,
),
style: AppTextStyles.summarySubtitle,
),
],
),
@@ -514,7 +512,7 @@ class ChatMessagesView extends ConsumerWidget {
} else if (isExercise) {
// 运动计划 — 主展示区大号显示运动名,字段列时长/频率/天数
title = '运动计划确认';
titleIcon = Icons.directions_run_outlined;
titleIcon = LucideIcons.footprints;
final exName =
(meta['value'] ??
meta['name'] ??
@@ -706,7 +704,7 @@ class ChatMessagesView extends ConsumerWidget {
: Icon(
isMedication
? Icons.medication_liquid_outlined
: Icons.directions_run_outlined,
: LucideIcons.footprints,
size: 36,
color: AppColors.iconColor,
),
@@ -895,9 +893,11 @@ class ChatMessagesView extends ConsumerWidget {
.read(chatProvider.notifier)
.confirmMessage(msg.id);
if (error != null && context.mounted) {
ScaffoldMessenger.of(
AppToast.show(
context,
).showSnackBar(SnackBar(content: Text(error)));
error,
type: AppToastType.error,
);
}
},
borderRadius: BorderRadius.circular(18),
@@ -1098,63 +1098,13 @@ class ChatMessagesView extends ConsumerWidget {
if (isUser)
SelectableText(
msg.content,
style: const TextStyle(
fontSize: 19,
color: Colors.white,
height: 1.5,
),
style: AppTextStyles.chatBody.copyWith(color: Colors.white),
)
else
MarkdownBody(
data: _cleanAiText(msg.content),
selectable: true,
AiMarkdownView(
data: msg.content,
onTapLink: (text, href, title) =>
_handleMarkdownLink(context, ref, href),
styleSheet: MarkdownStyleSheet(
p: const TextStyle(
fontSize: 19,
color: AppColors.textPrimary,
height: 1.5,
),
a: const TextStyle(
fontSize: 19,
color: AppColors.primary,
fontWeight: FontWeight.w700,
decoration: TextDecoration.underline,
decorationColor: AppColors.primary,
),
strong: const TextStyle(
fontSize: 19,
color: AppColors.textPrimary,
height: 1.5,
fontWeight: FontWeight.w700,
),
h1: const TextStyle(
fontSize: 22,
color: AppColors.textPrimary,
height: 1.35,
fontWeight: FontWeight.w800,
),
h2: const TextStyle(
fontSize: 21,
color: AppColors.textPrimary,
height: 1.4,
fontWeight: FontWeight.w800,
),
h3: const TextStyle(
fontSize: 20,
color: AppColors.textPrimary,
height: 1.4,
fontWeight: FontWeight.w800,
),
listBullet: const TextStyle(
fontSize: 24,
color: AppColors.textPrimary,
height: 1.2,
fontWeight: FontWeight.w900,
),
listBulletPadding: const EdgeInsets.only(right: 8),
),
),
// 图片缩略图(在文字下方)
@@ -1321,6 +1271,7 @@ class ChatMessagesView extends ConsumerWidget {
}
/// 清理 AI 返回文本中的异常占位符Markdown 格式交给 MarkdownBody 渲染)
// ignore: unused_element
static String _cleanAiText(String text) {
var t = text;
// 移除 $1、$2 等占位符
@@ -1449,7 +1400,21 @@ class ChatMessagesView extends ConsumerWidget {
}
static _AgentColors _agentColors(ActiveAgent agent) {
_AgentColors fromModule(AppModuleVisual visual) => _AgentColors(
gradient: [visual.gradient.colors.first, visual.gradient.colors.last],
bg: visual.lightColor,
border: visual.borderColor,
iconBg: visual.lightColor,
accent: visual.color,
verticalGradient: true,
);
return switch (agent) {
ActiveAgent.health => fromModule(AppModuleVisuals.health),
ActiveAgent.diet => fromModule(AppModuleVisuals.diet),
ActiveAgent.medication => fromModule(AppModuleVisuals.medication),
ActiveAgent.report => fromModule(AppModuleVisuals.report),
ActiveAgent.exercise => fromModule(AppModuleVisuals.exercise),
ActiveAgent.consultation => _AgentColors(
gradient: [Color(0xFFFFCAD4), Color(0xFFFF9AAE)],
bg: const Color(0xFFFFF4F6),
@@ -1458,46 +1423,6 @@ class ChatMessagesView extends ConsumerWidget {
accent: const Color(0xFFFF7F98),
verticalGradient: true,
),
ActiveAgent.health => _AgentColors(
gradient: [Color(0xFFA1FFCE), Color(0xFF69DB8F)],
bg: const Color(0xFFF4FEF2),
border: const Color(0xFFD4F0C8),
iconBg: const Color(0xFFF0FCEF),
accent: const Color(0xFF52B87A),
verticalGradient: true,
),
ActiveAgent.diet => _AgentColors(
gradient: [Color(0xFFF6D365), Color(0xFFFDA085)],
bg: const Color(0xFFFFF7F0),
border: const Color(0xFFFFE8D4),
iconBg: const Color(0xFFFFF0E4),
accent: const Color(0xFFF89C5B),
verticalGradient: true,
),
ActiveAgent.medication => _AgentColors(
gradient: [Color(0xFF89F7FE), Color(0xFF66A6FF)],
bg: const Color(0xFFF4F6FF),
border: const Color(0xFFDDE6FF),
iconBg: const Color(0xFFEFF3FF),
accent: const Color(0xFF4F8FF7),
verticalGradient: true,
),
ActiveAgent.report => _AgentColors(
gradient: [Color(0xFFE0C3FC), Color(0xFF8EC5FC)],
bg: const Color(0xFFF8F5FF),
border: const Color(0xFFE9E0FF),
iconBg: const Color(0xFFF2EDFF),
accent: const Color(0xFF9D8AF8),
verticalGradient: true,
),
ActiveAgent.exercise => _AgentColors(
gradient: [Color(0xFFB6F2FF), Color(0xFF7DD3FC)],
bg: const Color(0xFFF0FCFF),
border: const Color(0xFFD8F5FC),
iconBg: const Color(0xFFEAFBFF),
accent: const Color(0xFF38BDE8),
verticalGradient: true,
),
_ => _AgentColors(
gradient: [AppColors.primary, AppColors.blueMeasure],
bg: const Color(0xFFF6F3FF),
@@ -1519,7 +1444,7 @@ class ChatMessagesView extends ConsumerWidget {
'AI智能问诊描述症状获取建议',
),
ActiveAgent.report => (LucideIcons.fileText, '报告分析', '上传体检报告AI 辅助解读'),
ActiveAgent.exercise => (LucideIcons.activity, '运动', '制定运动计划,打卡记录进度'),
ActiveAgent.exercise => (LucideIcons.footprints, '运动', '制定运动计划,打卡记录进度'),
_ => (Icons.forum_outlined, 'AI 助手', '血管病患者的 AI 健康管理助手'),
};
}
@@ -1621,8 +1546,8 @@ class ChatMessagesView extends ConsumerWidget {
// ── 1. 健康指标 ──
// 没记录则整行不显示(避免每天唠叨提醒)
const healthIconColor = Color(0xFF3B82F6);
const healthIconBg = Color(0xFFDBEAFE);
final healthIconColor = AppModuleVisuals.health.color;
final healthIconBg = AppModuleVisuals.health.lightColor;
if (allNull) {
// 不显示
} else if (hasAbnormal) {
@@ -1632,7 +1557,7 @@ class ChatMessagesView extends ConsumerWidget {
tasks.add(
_taskRow(
context,
Icons.warning_amber_rounded,
LucideIcons.triangleAlert,
'健康指标',
trailing: trailing,
status: 'warning',
@@ -1646,7 +1571,7 @@ class ChatMessagesView extends ConsumerWidget {
tasks.add(
_taskRow(
context,
Icons.check_circle,
LucideIcons.circleCheck,
'健康指标',
trailing: '指标正常',
status: 'done',
@@ -1658,8 +1583,8 @@ class ChatMessagesView extends ConsumerWidget {
}
// ── 2. 运动 ──
const exIconColor = Color(0xFFF59E0B);
const exIconBg = Color(0xFFFEF3C7);
final exIconColor = AppModuleVisuals.exercise.color;
final exIconBg = AppModuleVisuals.exercise.lightColor;
final exercisePlan = ref.watch(currentExercisePlanProvider);
var hasExercise = false;
exercisePlan.when(
@@ -1685,7 +1610,7 @@ class ChatMessagesView extends ConsumerWidget {
tasks.add(
_taskRow(
context,
Icons.directions_run,
LucideIcons.footprints,
'$name $dur分钟',
status: done
? 'done'
@@ -1703,7 +1628,7 @@ class ChatMessagesView extends ConsumerWidget {
tasks.add(
_taskRow(
context,
Icons.directions_run,
LucideIcons.footprints,
'运动',
trailing: '正在加载运动计划',
status: 'pending',
@@ -1718,7 +1643,7 @@ class ChatMessagesView extends ConsumerWidget {
tasks.add(
_taskRow(
context,
Icons.directions_run,
LucideIcons.footprints,
'运动',
trailing: '运动计划加载失败',
status: 'overdue',
@@ -1733,7 +1658,7 @@ class ChatMessagesView extends ConsumerWidget {
tasks.add(
_taskRow(
context,
Icons.directions_run,
LucideIcons.footprints,
'运动',
trailing: '暂无今日运动计划',
status: 'pending',
@@ -1746,8 +1671,8 @@ class ChatMessagesView extends ConsumerWidget {
// ── 3. 用药打卡 ──
// 漏服时突出提醒;未到服药时间时也保留一行轻提示,避免今日健康缺少用药状态。
const medIconColor = Color(0xFFEC4899);
const medIconBg = Color(0xFFFCE7F3);
final medIconColor = AppModuleVisuals.medication.color;
final medIconBg = AppModuleVisuals.medication.lightColor;
reminders.whenOrNull(
data: (meds) {
final overdueMeds = meds
@@ -1757,7 +1682,7 @@ class ChatMessagesView extends ConsumerWidget {
tasks.add(
_taskRow(
context,
Icons.medication_rounded,
LucideIcons.pill,
'用药',
trailing: meds.isEmpty ? '暂无用药安排' : '暂时不用吃药',
status: 'done',
@@ -1815,7 +1740,7 @@ class ChatMessagesView extends ConsumerWidget {
tasks.add(
_taskRow(
context,
Icons.medication_rounded,
LucideIcons.pill,
title,
trailing: trailing,
status: status,