fix: VLM识别修复 + 饮食CRUD + 侧边栏美化 + UI优化
- VLM: ChatMessage.Content string→object, 修复视觉content双重序列化 - 饮食: diet/medication端点 record→手动JSON解析, 修复循环引用 - 饮食记录: 左滑删除 + 去评分 + AI饮食评语(DeepSeek) - 侧边栏: 功能区统一Row+Expanded, 服务包compact模式 - 历史对话: 点击加载历史消息 - 登录页: 居中布局, 去底部空白 - UI: 主色加深#6C5CE7, maxWidth:1024防图片超大
This commit is contained in:
@@ -4,29 +4,69 @@ import '../core/navigation_provider.dart';
|
||||
import '../providers/auth_provider.dart';
|
||||
import '../providers/data_providers.dart';
|
||||
|
||||
/// 饮食记录列表
|
||||
class DietRecordListPage extends ConsumerWidget {
|
||||
/// 饮食记录列表(左滑删除)
|
||||
class DietRecordListPage extends ConsumerStatefulWidget {
|
||||
const DietRecordListPage({super.key});
|
||||
@override Widget build(BuildContext context, WidgetRef ref) {
|
||||
final service = ref.watch(dietServiceProvider);
|
||||
@override ConsumerState<DietRecordListPage> createState() => _DietRecordListPageState();
|
||||
}
|
||||
class _DietRecordListPageState extends ConsumerState<DietRecordListPage> {
|
||||
Future<List<Map<String, dynamic>>>? _future;
|
||||
|
||||
@override void initState() { super.initState(); _refresh(); }
|
||||
void _refresh() => _future = ref.read(dietServiceProvider).getRecords();
|
||||
|
||||
@override Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF8F9FC),
|
||||
appBar: AppBar(title: const Text('饮食记录')),
|
||||
body: FutureBuilder<List<Map<String, dynamic>>>(
|
||||
future: service.getRecords(),
|
||||
future: _future,
|
||||
builder: (ctx, snap) {
|
||||
if (snap.connectionState == ConnectionState.waiting) return const Center(child: CircularProgressIndicator());
|
||||
if (!snap.hasData || snap.data!.isEmpty) return _empty(context, '饮食记录', '暂无饮食记录,可通过「拍饮食」录入');
|
||||
if (snap.connectionState == ConnectionState.waiting) return const Center(child: CircularProgressIndicator(color: Color(0xFF6C5CE7)));
|
||||
final data = snap.data ?? [];
|
||||
if (data.isEmpty) return _empty(context, '饮食记录', '暂无饮食记录,可通过「拍饮食」录入');
|
||||
return ListView.builder(
|
||||
itemCount: snap.data!.length,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
itemCount: data.length,
|
||||
itemBuilder: (ctx, i) {
|
||||
final d = snap.data![i];
|
||||
final d = data[i];
|
||||
final items = (d['foodItems'] as List?)?.cast<Map<String, dynamic>>() ?? [];
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
child: ListTile(
|
||||
title: Text('${d['mealType'] ?? ''} ${d['totalCalories'] ?? 0}千卡'),
|
||||
subtitle: Text(items.map((f) => f['name']).join(' | ')),
|
||||
trailing: _starWidget(d['healthScore']),
|
||||
final mealNames = {'Breakfast':'早餐','Lunch':'午餐','Dinner':'晚餐','Snack':'加餐'};
|
||||
final mealLabel = mealNames[d['mealType']?.toString()] ?? d['mealType']?.toString() ?? '';
|
||||
final mealIcons = {'Breakfast':'🌅','Lunch':'☀️','Dinner':'🌙','Snack':'🍪'};
|
||||
return Dismissible(
|
||||
key: Key(d['id']?.toString() ?? '$i'),
|
||||
direction: DismissDirection.endToStart,
|
||||
background: Container(
|
||||
alignment: Alignment.centerRight,
|
||||
padding: const EdgeInsets.only(right: 24),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
decoration: BoxDecoration(color: const Color(0xFFE53935), borderRadius: BorderRadius.circular(16)),
|
||||
child: const Icon(Icons.delete_outline, color: Colors.white, size: 28),
|
||||
),
|
||||
confirmDismiss: (_) async {
|
||||
try { await ref.read(dietServiceProvider).deleteRecord(d['id']?.toString() ?? ''); } catch (_) {}
|
||||
setState(_refresh);
|
||||
return false;
|
||||
},
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16)),
|
||||
child: Row(children: [
|
||||
Container(width: 44, height: 44, decoration: BoxDecoration(color: const Color(0xFFEDEAFF), borderRadius: BorderRadius.circular(12)), child: Center(child: Text(mealIcons[d['mealType']?.toString()] ?? '🍽️', style: const TextStyle(fontSize: 20)))),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Row(children: [
|
||||
Text(mealLabel, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600)),
|
||||
const SizedBox(width: 8),
|
||||
Text('${d['totalCalories'] ?? 0}千卡', style: const TextStyle(fontSize: 13, color: Color(0xFF6C5CE7))),
|
||||
]),
|
||||
const SizedBox(height: 4),
|
||||
Text(items.map((f) => f['name']).join(' · '), style: const TextStyle(fontSize: 13, color: Color(0xFF888888)), maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
])),
|
||||
const Icon(Icons.chevron_right, size: 18, color: Color(0xFFCCCCCC)),
|
||||
]),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -35,10 +75,6 @@ class DietRecordListPage extends ConsumerWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
Widget _starWidget(dynamic score) {
|
||||
final s = score is int ? score : 3;
|
||||
return Row(mainAxisSize: MainAxisSize.min, children: List.generate(5, (i) => Icon(Icons.star, size: 16, color: i < s ? const Color(0xFFF9A825) : Colors.grey[300])));
|
||||
}
|
||||
}
|
||||
|
||||
/// 运动计划页
|
||||
@@ -752,10 +788,8 @@ class DeviceManagementPage extends ConsumerWidget {
|
||||
@override Widget build(BuildContext context, WidgetRef ref) => _empty(context, '设备管理', '暂无绑定设备');
|
||||
}
|
||||
|
||||
Widget _empty(BuildContext context, String title, String subtitle) => Scaffold(
|
||||
appBar: AppBar(title: Text(title)),
|
||||
body: Center(child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
Widget _empty(BuildContext context, String title, String subtitle) =>
|
||||
Center(child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
Icon(Icons.inbox_outlined, size: 64, color: Colors.grey[300]),
|
||||
const SizedBox(height: 12), Text(subtitle, style: Theme.of(context).textTheme.bodyMedium),
|
||||
])),
|
||||
);
|
||||
]));
|
||||
|
||||
Reference in New Issue
Block a user