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:
@@ -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)),
|
||||
),
|
||||
),
|
||||
]),
|
||||
|
||||
@@ -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); },
|
||||
|
||||
@@ -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),
|
||||
]),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user