feat: 全面UI改造 — 报告/饮食/运动/用药/侧边栏/对话流/胶囊

- 报告模块:重写AI解读页,统一白色卡片+三栏布局(指标→解读→医生审核);加删除功能+后端删接口;VLM自动识别报告类型生成中文标题;去五颜六色
- 饮食分析:全面重设计,暖橙主题配色,图片自适应,餐次emoji选择器,去紫色
- 运动确认卡片:修复duration_minutes JSON类型转换,支持中文数字识别(三十分钟/半小时→30);主区域只显示运动名,字段行改为横向排列
- 流式输出:简化为最基础逐字追加,去buffer+Timer+淡入动画
- 欢迎卡片:每智能体独立渐变色(青/橙/蓝/紫/绿/粉),加卡片入场滑入+淡入动画(600ms)
- 确认卡片:头部去紫色背景改浅灰白,字段行去图标改纯信息横排,编辑框去双重边框
- 用药管理:胶囊改TabBar,添加按钮改右下FAB;打卡按钮改对号圆圈形式;药丸图标白底
- 侧边栏:加VIP服务/保险栏+图标,标题字体放大;服务包去查看更多+去VIP金色标签
- 胶囊:首页智能体胶囊白底深色字; side胶囊加阴影
- 对话流:reverse=false,今日健康出现在顶部; 对话页input bar匹配主页面样式
- 其他:个人资料去紫色+去多余入口;设置页白底图标;蓝牙页加返回按钮;报告管理改名
- 后端:加DELETE /api/reports/{id}; 修复manage_exercise数据类型转换;AI提示优化中文数字
This commit is contained in:
MingNian
2026-06-10 23:46:45 +08:00
parent 63d2092c24
commit f6dd14be3b
28 changed files with 1153 additions and 1578 deletions

View File

@@ -15,36 +15,40 @@ class DietRecordListPage extends ConsumerStatefulWidget {
@override ConsumerState<DietRecordListPage> createState() => _DietRecordListPageState();
}
class _DietRecordListPageState extends ConsumerState<DietRecordListPage> {
Future<List<Map<String, dynamic>>>? _future;
List<Map<String, dynamic>> _data = [];
bool _loading = true;
@override void initState() { super.initState(); _refresh(); }
void _refresh() { setState(() { _future = ref.read(dietServiceProvider).getRecords(); }); }
Future<void> _refresh() async {
final records = await ref.read(dietServiceProvider).getRecords();
if (mounted) setState(() { _data = records; _loading = false; });
}
Future<void> _delete(String id) async {
await ref.read(dietServiceProvider).deleteRecord(id);
if (mounted) setState(() => _data.removeWhere((d) => d['id']?.toString() == id));
}
@override Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppTheme.bg,
return GradientScaffold(
appBar: AppBar(leading: IconButton(icon: const Icon(Icons.arrow_back), onPressed: () => popRoute(ref)), title: const Text('饮食记录')),
body: FutureBuilder<List<Map<String, dynamic>>>(
future: _future,
builder: (ctx, snap) {
if (snap.connectionState == ConnectionState.waiting) return const Center(child: CircularProgressIndicator(color: AppTheme.primary));
final data = snap.data ?? [];
if (data.isEmpty) return _empty(context, '饮食记录', '暂无饮食记录,可通过「拍饮食」录入');
return ListView.builder(
body: Container(
decoration: const BoxDecoration(gradient: AppColors.bgGradient),
child: _loading
? const Center(child: CircularProgressIndicator(color: AppTheme.primary))
: _data.isEmpty
? _empty(context, '饮食记录', '暂无饮食记录,可通过「拍饮食」录入')
: ListView.builder(
padding: const EdgeInsets.symmetric(vertical: 8),
itemCount: data.length,
itemCount: _data.length,
itemBuilder: (ctx, i) {
final d = data[i];
final d = _data[i];
final items = (d['foodItems'] as List?)?.cast<Map<String, dynamic>>() ?? [];
final mealNames = {'Breakfast':'早餐','Lunch':'午餐','Dinner':'晚餐','Snack':'加餐'};
final mealLabel = mealNames[d['mealType']?.toString()] ?? d['mealType']?.toString() ?? '';
final mealIcons = {'Breakfast':'🌅','Lunch':'☀️','Dinner':'🌙','Snack':'🍪'};
return SwipeDeleteTile(
key: Key(d['id']?.toString() ?? '$i'),
onDelete: () async {
await ref.read(dietServiceProvider).deleteRecord(d['id']?.toString() ?? '');
_refresh();
},
onDelete: () => _delete(d['id']?.toString() ?? ''),
onTap: () => pushRoute(ref, 'dietDetail', params: {'id': d['id']?.toString() ?? ''}),
child: Container(
padding: const EdgeInsets.all(14),
@@ -66,8 +70,7 @@ class _DietRecordListPageState extends ConsumerState<DietRecordListPage> {
),
);
},
);
},
),
),
);
}
@@ -79,8 +82,7 @@ class DietRecordDetailPage extends ConsumerWidget {
const DietRecordDetailPage({super.key, required this.id});
@override Widget build(BuildContext context, WidgetRef ref) {
final service = ref.watch(dietServiceProvider);
return Scaffold(
backgroundColor: AppTheme.bg,
return GradientScaffold(
appBar: AppBar(leading: IconButton(icon: const Icon(Icons.arrow_back), onPressed: () => popRoute(ref)), title: const Text('饮食详情')),
body: FutureBuilder<List<Map<String, dynamic>>>(
future: service.getRecords(),
@@ -143,11 +145,12 @@ class _ExercisePlanPageState extends ConsumerState<ExercisePlanPage> {
}
@override Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppTheme.bg,
return GradientScaffold(
appBar: AppBar(leading: IconButton(icon: const Icon(Icons.arrow_back), onPressed: () => popRoute(ref)), title: const Text('运动计划')),
floatingActionButton: FloatingActionButton(onPressed: () { pushRoute(ref, 'exerciseCreate'); }, backgroundColor: AppTheme.primary, child: const Icon(Icons.add)),
body: FutureBuilder<List<Map<String, dynamic>>>(
body: Container(
decoration: const BoxDecoration(gradient: AppColors.bgGradient),
child: FutureBuilder<List<Map<String, dynamic>>>(
future: _future,
builder: (ctx, snap) {
final plans = snap.data ?? [];
@@ -188,11 +191,10 @@ class _ExercisePlanPageState extends ConsumerState<ExercisePlanPage> {
])),
GestureDetector(
onTap: hasTodayItem ? () => _checkIn(p['id']?.toString() ?? '', todayItem['id']?.toString() ?? '') : null,
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
child: Container(
width: 40, height: 40,
decoration: BoxDecoration(
color: todayDone ? AppColors.successLight : (hasTodayItem ? AppTheme.primaryLight : AppColors.cardInner),
color: todayDone ? AppColors.successLight : AppColors.cardInner,
borderRadius: BorderRadius.circular(12),
),
child: Icon(todayDone ? Icons.check_circle : Icons.check_circle_outline, size: 28, color: todayDone ? AppTheme.success : AppColors.textHint),
@@ -204,6 +206,7 @@ class _ExercisePlanPageState extends ConsumerState<ExercisePlanPage> {
});
},
),
),
);
}
}
@@ -235,8 +238,7 @@ class _ExercisePlanCreatePageState extends ConsumerState<ExercisePlanCreatePage>
}
@override Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppTheme.bg,
return GradientScaffold(
appBar: AppBar(leading: IconButton(icon: const Icon(Icons.arrow_back), onPressed: () => popRoute(ref)), title: const Text('新建计划')),
body: ListView(padding: const EdgeInsets.all(16), children: [
_field('运动名称', _nameCtrl, hint: '如: 散步、慢跑、太极'),
@@ -256,7 +258,7 @@ class _ExercisePlanCreatePageState extends ConsumerState<ExercisePlanCreatePage>
return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(label, style: TextStyle(fontSize: 17, color: AppColors.textSecondary)),
const SizedBox(height: 6),
TextField(controller: ctrl, keyboardType: number ? TextInputType.number : null, decoration: InputDecoration(hintText: hint, filled: true, fillColor: AppColors.backgroundSoft, border: OutlineInputBorder(borderRadius: BorderRadius.circular(AppTheme.rMd), borderSide: BorderSide.none)), style: const TextStyle(fontSize: 19)),
TextField(controller: ctrl, keyboardType: number ? TextInputType.number : null, decoration: InputDecoration(hintText: hint, filled: true, fillColor: Colors.white, border: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: BorderSide(color: AppColors.border)), focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: const BorderSide(color: AppColors.primary, width: 1.5))), style: const TextStyle(fontSize: 17)),
]);
}
@@ -287,7 +289,7 @@ class _FollowUpListPageState extends ConsumerState<FollowUpListPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
return GradientScaffold(
appBar: AppBar(leading: IconButton(icon: const Icon(Icons.arrow_back), onPressed: () => popRoute(ref)), title: const Text('复查随访'), centerTitle: true),
body: FutureBuilder<List<Map<String, dynamic>>>(
future: _future,
@@ -478,14 +480,12 @@ class _HealthArchivePageState extends ConsumerState<HealthArchivePage> {
@override Widget build(BuildContext context) {
if (_loading) {
return Scaffold(
backgroundColor: AppColors.background,
return GradientScaffold(
appBar: AppBar(leading: IconButton(icon: const Icon(Icons.arrow_back), onPressed: () => popRoute(ref)), title: const Text('健康档案')),
body: const Center(child: CircularProgressIndicator(color: AppColors.primary)),
);
}
return Scaffold(
backgroundColor: AppColors.background,
return GradientScaffold(
appBar: AppBar(
backgroundColor: AppColors.cardBackground,
title: const Text('健康档案'),
@@ -549,6 +549,7 @@ class _HealthArchivePageState extends ConsumerState<HealthArchivePage> {
decoration: BoxDecoration(
color: AppColors.cardBackground,
borderRadius: BorderRadius.circular(AppTheme.rMd),
border: Border.all(color: Color(0xFFC8DDFD), width: 1.5),
boxShadow: [AppTheme.shadowCard],
),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
@@ -556,7 +557,7 @@ class _HealthArchivePageState extends ConsumerState<HealthArchivePage> {
Container(
width: 36, height: 36,
decoration: BoxDecoration(
color: AppColors.iconBg,
gradient: AppColors.bgGradient,
borderRadius: BorderRadius.circular(10),
),
child: Icon(icon, size: 20, color: AppColors.primary),
@@ -572,7 +573,7 @@ class _HealthArchivePageState extends ConsumerState<HealthArchivePage> {
Widget _buildField(String label, TextEditingController ctrl, {String? hint}) {
return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(label, style: const TextStyle(fontSize: 14, color: AppColors.textHint)),
Text(label, style: const TextStyle(fontSize: 14, color: AppColors.textSecondary)),
const SizedBox(height: 6),
TextField(
controller: ctrl,
@@ -581,18 +582,18 @@ class _HealthArchivePageState extends ConsumerState<HealthArchivePage> {
hintText: hint,
hintStyle: const TextStyle(fontSize: 15, color: AppColors.textHint),
filled: true,
fillColor: AppColors.backgroundSoft,
fillColor: Colors.white,
contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppTheme.rSm),
borderSide: BorderSide.none,
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: AppColors.border),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppTheme.rSm),
borderSide: BorderSide.none,
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: AppColors.border),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppTheme.rSm),
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: AppColors.primary, width: 1.5),
),
),
@@ -634,7 +635,7 @@ class _HealthCalendarPageState extends ConsumerState<HealthCalendarPage> {
}
@override Widget build(BuildContext context) {
return Scaffold(
return GradientScaffold(
appBar: AppBar(leading: IconButton(icon: const Icon(Icons.arrow_back), onPressed: () => popRoute(ref)), title: const Text('健康日历'), centerTitle: true),
body: Column(children: [
_buildMonthHeader(),
@@ -847,7 +848,7 @@ class StaticTextPage extends ConsumerWidget {
© 2025-2026 健康管家团队。保留所有权利。
本软件受中华人民共和国著作权法保护。''',
};
return Scaffold(
return GradientScaffold(
appBar: AppBar(
backgroundColor: Colors.white,
elevation: 0,
@@ -869,8 +870,7 @@ class DeviceManagementPage extends ConsumerWidget {
@override Widget build(BuildContext context, WidgetRef ref) {
final device = ref.watch(omronDeviceProvider);
return Scaffold(
backgroundColor: AppColors.background,
return GradientScaffold(
appBar: AppBar(
backgroundColor: AppColors.cardBackground,
title: const Text('蓝牙血压计'),