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

@@ -35,8 +35,7 @@ class _MedicationCheckInPageState extends ConsumerState<MedicationCheckInPage> {
}
@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('服药打卡'), centerTitle: true),
body: FutureBuilder<List<Map<String, dynamic>>>(
future: _future,
@@ -120,14 +119,16 @@ class _MedicationCheckInPageState extends ConsumerState<MedicationCheckInPage> {
GestureDetector(
onTap: () => _toggleDose(medId, time, isTaken),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
width: 40, height: 40,
decoration: BoxDecoration(
color: isTaken ? AppColors.successLight : AppTheme.primary,
borderRadius: BorderRadius.circular(20),
color: isTaken ? AppColors.successLight : AppColors.cardInner,
borderRadius: BorderRadius.circular(12),
),
child: Icon(
isTaken ? Icons.check_circle : Icons.check_circle_outline,
size: 28,
color: isTaken ? AppTheme.success : AppColors.textHint,
),
child: Text(isTaken ? '已打卡' : '打卡',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600,
color: isTaken ? AppTheme.success : AppColors.textOnGradient)),
),
),
]),

View File

@@ -72,8 +72,7 @@ class _MedicationEditPageState extends ConsumerState<MedicationEditPage> {
}
@override Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppTheme.bg,
return GradientScaffold(
appBar: AppBar(title: Text(widget.id != null ? '编辑用药' : '添加用药')),
body: ListView(padding: const EdgeInsets.all(16), children: [
// 名称+剂量 一行
@@ -95,8 +94,8 @@ class _MedicationEditPageState extends ConsumerState<MedicationEditPage> {
if (t != null) setState(() => _times[i] = t);
},
child: Container(padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(color: AppTheme.primaryLight, borderRadius: BorderRadius.circular(12)),
child: Text('${_times[i].hour.toString().padLeft(2,'0')}:${_times[i].minute.toString().padLeft(2,'0')}', style: const TextStyle(fontSize: 18, color: AppTheme.primary, fontWeight: FontWeight.w600))),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12), border: Border.all(color: AppColors.border, width: 1)),
child: Text('${_times[i].hour.toString().padLeft(2,'0')}:${_times[i].minute.toString().padLeft(2,'0')}', style: const TextStyle(fontSize: 18, color: AppColors.textPrimary, fontWeight: FontWeight.w600))),
))),
const SizedBox(height: 16),
// 开始+结束 一行
@@ -108,7 +107,7 @@ class _MedicationEditPageState extends ConsumerState<MedicationEditPage> {
const SizedBox(height: 16),
_field('备注', _notesCtrl, hint: '如: 饭后服用、睡前'),
const SizedBox(height: 32),
SizedBox(width: double.infinity, child: ElevatedButton(onPressed: _loading ? null : _save, style: ElevatedButton.styleFrom(backgroundColor: AppTheme.primary, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), padding: const EdgeInsets.symmetric(vertical: 14)), child: Text(_loading ? '保存中...' : '保存', style: const TextStyle(fontSize: 19)))),
SizedBox(width: double.infinity, child: ElevatedButton(onPressed: _loading ? null : _save, style: ElevatedButton.styleFrom(backgroundColor: Colors.white, foregroundColor: AppColors.primary, side: const BorderSide(color: AppColors.primary, width: 1.5), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), padding: const EdgeInsets.symmetric(vertical: 14)), child: Text(_loading ? '保存中...' : '保存', style: const TextStyle(fontSize: 19, fontWeight: FontWeight.w600)))),
const SizedBox(height: 20),
]),
);
@@ -119,7 +118,7 @@ class _MedicationEditPageState extends ConsumerState<MedicationEditPage> {
_label(label), const SizedBox(height: 6),
TextField(controller: ctrl, decoration: InputDecoration(hintText: hint, filled: true, fillColor: AppTheme.surface, border: OutlineInputBorder(borderRadius: BorderRadius.circular(AppTheme.rMd), borderSide: BorderSide.none), contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12)), style: const TextStyle(fontSize: 19)),
]);
Widget _chip(String label, bool sel, VoidCallback onTap) => GestureDetector(onTap: onTap, child: Container(padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration(color: sel ? AppTheme.primary : AppTheme.primaryLight, borderRadius: BorderRadius.circular(16)), child: Text(label, style: TextStyle(fontSize: 16, color: sel ? Colors.white : AppTheme.primary, fontWeight: FontWeight.w500))));
Widget _chip(String label, bool sel, VoidCallback onTap) => GestureDetector(onTap: onTap, child: Container(padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration(color: sel ? Colors.white : AppColors.cardInner, borderRadius: BorderRadius.circular(16), border: sel ? Border.all(color: AppColors.primary, width: 1.5) : null), child: Text(label, style: TextStyle(fontSize: 16, color: sel ? AppColors.primary : AppColors.textSecondary, fontWeight: FontWeight.w500))));
Widget _dateField(String label, DateTime val, ValueChanged<DateTime> cb) => Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
_label(label), const SizedBox(height: 6),
GestureDetector(onTap: () async { final d = await showDatePicker(context: context, initialDate: val, firstDate: DateTime(2024), lastDate: DateTime(2030)); if (d != null) cb(d); },

View File

@@ -1,11 +1,10 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import '../../core/app_colors.dart';
import '../../core/app_theme.dart';
import '../../core/navigation_provider.dart';
import '../../providers/data_providers.dart';
import '../../widgets/app_empty_state.dart';
import '../../widgets/app_tab_chip.dart';
import '../../widgets/common_widgets.dart';
class MedicationListPage extends ConsumerStatefulWidget {
@@ -13,11 +12,23 @@ class MedicationListPage extends ConsumerStatefulWidget {
@override ConsumerState<MedicationListPage> createState() => _MedicationListPageState();
}
class _MedicationListPageState extends ConsumerState<MedicationListPage> {
class _MedicationListPageState extends ConsumerState<MedicationListPage> with SingleTickerProviderStateMixin {
String _filter = '';
Future<List<Map<String, dynamic>>>? _future;
late final _tabCtrl = TabController(length: 3, vsync: this);
@override void initState() {
super.initState();
_tabCtrl.addListener(() {
if (!_tabCtrl.indexIsChanging) {
setState(() { _filter = ['', 'active', 'inactive'][_tabCtrl.index]; });
_load();
}
});
_load();
}
@override void dispose() { _tabCtrl.dispose(); super.dispose(); }
@override void initState() { super.initState(); _load(); }
void _load() { setState(() { _future = ref.read(medicationServiceProvider).getMedications(_filter); }); }
Future<void> _delete(String id) async {
@@ -26,100 +37,101 @@ class _MedicationListPageState extends ConsumerState<MedicationListPage> {
_load();
}
static const _tabs = [('全部', ''), ('服用中', 'active'), ('已停药', 'inactive')];
static const _tabs = ['全部', '服用中', '已停药'];
@override Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppTheme.bg,
appBar: AppBar(
leading: IconButton(icon: const Icon(Icons.arrow_back), onPressed: () => popRoute(ref)),
title: const Text('用药管理'),
actions: [
ShadButton.ghost(
size: ShadButtonSize.sm,
child: const Text('添加'),
onPressed: () => pushRoute(ref, 'medicationEdit'),
bottom: PreferredSize(
preferredSize: const Size.fromHeight(48),
child: TabBar(
controller: _tabCtrl,
indicatorColor: AppColors.primary,
labelColor: AppColors.primary,
unselectedLabelColor: AppColors.textHint,
labelStyle: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600),
tabs: _tabs.map((t) => Tab(child: Text(t))).toList(),
),
],
),
body: Column(children: [
Padding(
padding: const EdgeInsets.all(AppTheme.sMd),
child: Row(children: _tabs.map((t) => AppTabChip(
label: t.$1,
selected: _filter == t.$2,
onTap: () { _filter = t.$2; _load(); },
)).toList()),
),
Expanded(child: FutureBuilder<List<Map<String, dynamic>>>(
future: _future,
builder: (ctx, snap) {
final list = snap.data ?? [];
if (list.isEmpty) {
return AppEmptyState(
icon: LucideIcons.pill,
),
floatingActionButton: FloatingActionButton(
onPressed: () => pushRoute(ref, 'medicationEdit'),
backgroundColor: AppColors.primary,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: const Icon(Icons.add, size: 28, color: Colors.white),
),
body: Container(
decoration: const BoxDecoration(gradient: AppColors.bgGradient),
child: FutureBuilder<List<Map<String, dynamic>>>(
future: _future,
builder: (ctx, snap) {
final list = snap.data ?? [];
if (list.isEmpty) {
return const AppEmptyState(
icon: Icons.medication_outlined,
title: '暂无用药',
subtitle: '点击右上角添加药品',
subtitle: '点击右下角➕添加药品',
);
}
return ListView.builder(
padding: const EdgeInsets.fromLTRB(0, 0, 0, AppTheme.sMd),
itemCount: list.length + 1,
itemBuilder: (ctx, i) {
if (i == list.length) return const SizedBox(height: 80);
final m = list[i];
final times = (m['timeOfDay'] as List?)?.map((t) => t.toString().substring(0, 5)).join(' ') ?? '';
final isActive = m['isActive'] == true;
final id = m['id']?.toString() ?? '';
return SwipeDeleteTile(
key: Key(id),
onDelete: () => _delete(id),
onTap: () => pushRoute(ref, 'medCheckIn', params: {'id': id}),
margin: const EdgeInsets.symmetric(horizontal: AppTheme.sLg, vertical: 4),
child: Container(
padding: const EdgeInsets.all(AppTheme.sLg),
decoration: BoxDecoration(
color: AppTheme.surface,
borderRadius: BorderRadius.circular(AppTheme.rMd),
boxShadow: [AppTheme.shadowLight],
),
child: Row(children: [
Container(
width: 44, height: 44,
decoration: BoxDecoration(
color: isActive ? AppTheme.primaryLight : AppTheme.bgSoft,
borderRadius: BorderRadius.circular(AppTheme.rSm),
),
child: Icon(LucideIcons.pill, size: 25, color: isActive ? AppTheme.primary : AppTheme.textSub),
),
const SizedBox(width: AppTheme.sMd),
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Row(children: [
Text(m['name']?.toString() ?? '', style: TextStyle(fontSize: 19, fontWeight: FontWeight.w600, color: isActive ? AppTheme.text : AppTheme.textSub)),
if (!isActive) ...[
const SizedBox(width: 6),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1),
decoration: BoxDecoration(
color: AppTheme.bgSoft,
borderRadius: BorderRadius.circular(4),
),
child: Text('已停', style: TextStyle(fontSize: 13, color: AppTheme.textSub)),
),
],
]),
const SizedBox(height: 2),
Text('${m['dosage'] ?? ''} $times', style: TextStyle(fontSize: 16, color: AppTheme.textSub)),
])),
Icon(LucideIcons.chevronRight, size: 21, color: AppTheme.border),
]),
}
return ListView.builder(
padding: const EdgeInsets.fromLTRB(0, 8, 0, 80),
itemCount: list.length,
itemBuilder: (ctx, i) {
final m = list[i];
final times = (m['timeOfDay'] as List?)?.map((t) => t.toString().substring(0, 5)).join(' ') ?? '';
final isActive = m['isActive'] == true;
final id = m['id']?.toString() ?? '';
return SwipeDeleteTile(
key: Key(id),
onDelete: () => _delete(id),
onTap: () => pushRoute(ref, 'medCheckIn', params: {'id': id}),
margin: const EdgeInsets.symmetric(horizontal: AppTheme.sLg, vertical: 4),
child: Container(
padding: const EdgeInsets.all(AppTheme.sLg),
decoration: BoxDecoration(
color: AppTheme.surface,
borderRadius: BorderRadius.circular(AppTheme.rMd),
boxShadow: [AppTheme.shadowLight],
),
);
},
);
},
)),
]),
child: Row(children: [
Container(
width: 44, height: 44,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(AppTheme.rSm),
),
child: Icon(Icons.medication_outlined, size: 25, color: isActive ? AppColors.primary : AppTheme.textSub),
),
const SizedBox(width: AppTheme.sMd),
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Row(children: [
Text(m['name']?.toString() ?? '', style: TextStyle(fontSize: 19, fontWeight: FontWeight.w600, color: isActive ? AppTheme.text : AppTheme.textSub)),
if (!isActive) ...[
const SizedBox(width: 6),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1),
decoration: BoxDecoration(
color: AppTheme.bgSoft,
borderRadius: BorderRadius.circular(4),
),
child: Text('已停', style: TextStyle(fontSize: 13, color: AppTheme.textSub)),
),
],
]),
const SizedBox(height: 2),
Text('${m['dosage'] ?? ''} $times', style: TextStyle(fontSize: 16, color: AppTheme.textSub)),
])),
Icon(Icons.chevron_right, size: 21, color: AppTheme.border),
]),
),
);
},
);
},
),
),
);
}
}