feat: UI 清新风全面改造 — 朝露主题 + shadcn_ui + 全局字号放大
- 全新"朝露"主题:深青绿主色(#2D6A4F),暖白底,完整设计Token - 引入 shadcn_ui 组件库,MaterialApp 注入 ShadTheme - 新增 4 个公共组件:AppCard、AppMenuItem、AppEmptyState、AppTabChip - 全面消除硬编码颜色,统一使用 AppTheme Token - 全局字号 +3~4pt,图标等比放大 +3px - home/medication/profile/settings/login 等核心页面重写 - 路由和功能逻辑零改动
This commit is contained in:
@@ -1,14 +1,16 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'dart:io';
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
import '../../core/app_theme.dart';
|
||||
import '../../core/navigation_provider.dart';
|
||||
import '../../providers/auth_provider.dart';
|
||||
import '../../providers/chat_provider.dart';
|
||||
import '../../providers/data_providers.dart';
|
||||
import '../diet/diet_capture_page.dart';
|
||||
import '../../core/navigation_provider.dart';
|
||||
import '../../widgets/health_drawer.dart';
|
||||
import '../diet/diet_capture_page.dart';
|
||||
import 'widgets/chat_messages_view.dart';
|
||||
|
||||
class HomePage extends ConsumerStatefulWidget {
|
||||
@@ -21,9 +23,10 @@ class _HomePageState extends ConsumerState<HomePage> {
|
||||
final _scrollCtrl = ScrollController();
|
||||
String? _pickedImagePath;
|
||||
final Set<ActiveAgent> _welcomedAgents = {};
|
||||
final _focusNode = FocusNode();
|
||||
|
||||
@override void initState() { super.initState(); }
|
||||
@override void dispose() { _textCtrl.dispose(); _scrollCtrl.dispose(); super.dispose(); }
|
||||
@override void dispose() { _textCtrl.dispose(); _scrollCtrl.dispose(); _focusNode.dispose(); super.dispose(); }
|
||||
|
||||
void _sendMessage() {
|
||||
final text = _textCtrl.text.trim();
|
||||
@@ -43,62 +46,60 @@ class _HomePageState extends ConsumerState<HomePage> {
|
||||
final auth = ref.watch(authProvider);
|
||||
final user = auth.user;
|
||||
final selectedAgent = ref.watch(selectedAgentProvider);
|
||||
final theme = ShadTheme.of(context);
|
||||
|
||||
ref.listen(cameraActionProvider, (prev, next) {
|
||||
if (next == 'camera') {
|
||||
_pickImage(ImageSource.camera);
|
||||
ref.read(cameraActionProvider.notifier).clear();
|
||||
} else if (next == 'gallery') {
|
||||
_pickImage(ImageSource.gallery);
|
||||
ref.read(cameraActionProvider.notifier).clear();
|
||||
}
|
||||
if (next == 'camera') { _pickImage(ImageSource.camera); ref.read(cameraActionProvider.notifier).clear(); }
|
||||
else if (next == 'gallery') { _pickImage(ImageSource.gallery); ref.read(cameraActionProvider.notifier).clear(); }
|
||||
});
|
||||
|
||||
ref.listen(dietActionProvider, (prev, next) {
|
||||
if (next == 'pickFoodCamera') {
|
||||
_pickFoodImage(ImageSource.camera);
|
||||
ref.read(dietActionProvider.notifier).clear();
|
||||
} else if (next == 'pickFoodGallery') {
|
||||
_pickFoodImage(ImageSource.gallery);
|
||||
ref.read(dietActionProvider.notifier).clear();
|
||||
}
|
||||
if (next == 'pickFoodCamera') { _pickFoodImage(ImageSource.camera); ref.read(dietActionProvider.notifier).clear(); }
|
||||
else if (next == 'pickFoodGallery') { _pickFoodImage(ImageSource.gallery); ref.read(dietActionProvider.notifier).clear(); }
|
||||
});
|
||||
|
||||
return Scaffold(
|
||||
drawer: const HealthDrawer(),
|
||||
backgroundColor: const Color(0xFFF8F9FC),
|
||||
backgroundColor: theme.colorScheme.background,
|
||||
body: SafeArea(
|
||||
child: Column(children: [
|
||||
// ── 顶部栏 ──
|
||||
_buildHeader(user),
|
||||
|
||||
// ── 聊天区域(今日任务在对话流最上方)──
|
||||
_buildHeader(user, theme),
|
||||
Expanded(child: ChatMessagesView(scrollCtrl: _scrollCtrl, messages: chatState.messages)),
|
||||
|
||||
// ── 底部合并区 ──
|
||||
_buildBottomBar(context, selectedAgent),
|
||||
_buildBottomBar(context, selectedAgent, theme),
|
||||
]),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ═════════════════════ 顶部栏 ═════════════════════
|
||||
|
||||
Widget _buildHeader(dynamic user) {
|
||||
// ═══════ 顶部栏 ═══════
|
||||
Widget _buildHeader(dynamic user, ShadThemeData theme) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||
padding: const EdgeInsets.symmetric(horizontal: AppTheme.sLg, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.card,
|
||||
border: Border(bottom: BorderSide(color: theme.colorScheme.border, width: 0.5)),
|
||||
),
|
||||
child: Row(children: [
|
||||
Builder(builder: (ctx) => GestureDetector(
|
||||
onTap: () => Scaffold.of(ctx).openDrawer(),
|
||||
child: CircleAvatar(radius: 20, backgroundColor: const Color(0xFFF0F2FF), backgroundImage: user?.avatarUrl != null ? NetworkImage(user!.avatarUrl!) : null, child: user?.avatarUrl == null ? const Icon(Icons.person, size: 24, color: Color(0xFF8B9CF7)) : null),
|
||||
child: CircleAvatar(
|
||||
radius: 20,
|
||||
backgroundColor: AppTheme.primaryLight,
|
||||
backgroundImage: user?.avatarUrl != null ? NetworkImage(user!.avatarUrl!) : null,
|
||||
child: user?.avatarUrl == null ? const Icon(Icons.person, size: 28, color: AppTheme.primary) : null,
|
||||
),
|
||||
)),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Row(mainAxisSize: MainAxisSize.min, children: [Icon(Icons.smart_toy_outlined, size: 16, color: const Color(0xFF8B9CF7)), const SizedBox(width: 4), Text('AI 健康管家', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: Colors.grey[600]))]),
|
||||
Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
Icon(LucideIcons.bot, size: 19, color: AppTheme.primary),
|
||||
const SizedBox(width: 4),
|
||||
Text('AI 健康管家', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: theme.colorScheme.mutedForeground)),
|
||||
]),
|
||||
const SizedBox(height: 2),
|
||||
Text('${_getGreeting()},${(user?.name != null && user!.name!.isNotEmpty) ? user.name : '用户'}!', style: const TextStyle(fontSize: 17, fontWeight: FontWeight.bold, color: Color(0xFF1A1A1A))),
|
||||
Text('${_getGreeting()},${(user?.name != null && user!.name!.isNotEmpty) ? user.name : '用户'}!',
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: theme.colorScheme.foreground)),
|
||||
])),
|
||||
Icon(Icons.notifications_none, size: 22, color: Colors.grey[600]),
|
||||
Icon(LucideIcons.bell, size: 25, color: theme.colorScheme.mutedForeground),
|
||||
]),
|
||||
);
|
||||
}
|
||||
@@ -111,21 +112,20 @@ class _HomePageState extends ConsumerState<HomePage> {
|
||||
return '晚上好';
|
||||
}
|
||||
|
||||
// ═════════════════════ 智能体选择条(常驻) ═════════════════════
|
||||
|
||||
// ═══════ 智能体选择条 ═══════
|
||||
static final _agentDefs = [
|
||||
(ActiveAgent.consultation, '问诊', Icons.chat_bubble_outline),
|
||||
(ActiveAgent.health, '记数据', Icons.favorite_border),
|
||||
(ActiveAgent.diet, '拍饮食', Icons.restaurant_outlined),
|
||||
(ActiveAgent.medication, '药管家', Icons.medication_outlined),
|
||||
(ActiveAgent.report, '看报告', Icons.description_outlined),
|
||||
(ActiveAgent.exercise, '运动', Icons.directions_run_outlined),
|
||||
(ActiveAgent.consultation, '问诊', LucideIcons.messageCircle),
|
||||
(ActiveAgent.health, '记数据', LucideIcons.heart),
|
||||
(ActiveAgent.diet, '拍饮食', LucideIcons.utensils),
|
||||
(ActiveAgent.medication, '药管家', LucideIcons.pill),
|
||||
(ActiveAgent.report, '看报告', LucideIcons.fileText),
|
||||
(ActiveAgent.exercise, '运动', LucideIcons.trendingUp),
|
||||
];
|
||||
|
||||
Widget _buildAgentBar(ActiveAgent? selected) {
|
||||
Widget _buildAgentBar(ActiveAgent? selected, ShadThemeData theme) {
|
||||
return Container(
|
||||
height: 36,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
height: 40,
|
||||
padding: const EdgeInsets.symmetric(horizontal: AppTheme.sMd),
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: _agentDefs.length,
|
||||
@@ -146,17 +146,18 @@ class _HomePageState extends ConsumerState<HomePage> {
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: isActive ? const Color(0xFF8B9CF7) : Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: isActive ? const Color(0xFF8B9CF7) : const Color(0xFFE0E0E0)),
|
||||
color: isActive ? AppTheme.primary : theme.colorScheme.card,
|
||||
borderRadius: BorderRadius.circular(AppTheme.rPill),
|
||||
boxShadow: isActive ? [AppTheme.shadowLight] : null,
|
||||
),
|
||||
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
Icon(icon, size: 13, color: isActive ? Colors.white : const Color(0xFF666666)),
|
||||
const SizedBox(width: 3),
|
||||
Text(label, style: TextStyle(fontSize: 11, fontWeight: isActive ? FontWeight.w600 : FontWeight.w500, color: isActive ? Colors.white : const Color(0xFF666666))),
|
||||
Icon(icon, size: 17, color: isActive ? Colors.white : theme.colorScheme.mutedForeground),
|
||||
const SizedBox(width: 4),
|
||||
Text(label, style: TextStyle(fontSize: 15, fontWeight: isActive ? FontWeight.w600 : FontWeight.w500, color: isActive ? Colors.white : theme.colorScheme.mutedForeground)),
|
||||
]),
|
||||
),
|
||||
);
|
||||
@@ -165,59 +166,137 @@ class _HomePageState extends ConsumerState<HomePage> {
|
||||
);
|
||||
}
|
||||
|
||||
// ═════════════════════ 底部合并区:智能体栏 + 操作面板 + 输入框 ═════════════════════
|
||||
|
||||
Widget _buildBottomBar(BuildContext context, ActiveAgent? selectedAgent) {
|
||||
// ═══════ 底部区域 ═══════
|
||||
Widget _buildBottomBar(BuildContext context, ActiveAgent? selectedAgent, ShadThemeData theme) {
|
||||
return Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
// 智能体胶囊栏(常驻,高度36)
|
||||
_buildAgentBar(selectedAgent),
|
||||
|
||||
// 图片预览(有选中图片时显示)
|
||||
if (_pickedImagePath != null) _buildImagePreview(),
|
||||
|
||||
// 输入框
|
||||
_buildCompactInputBar(context),
|
||||
Container(
|
||||
padding: const EdgeInsets.only(top: AppTheme.sSm, bottom: 6),
|
||||
child: _buildAgentBar(selectedAgent, theme),
|
||||
),
|
||||
if (_pickedImagePath != null) _buildImagePreview(theme),
|
||||
_buildCompactInputBar(theme),
|
||||
]);
|
||||
}
|
||||
|
||||
Widget _buildImagePreview() {
|
||||
Widget _buildImagePreview(ShadThemeData theme) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(12, 8, 12, 4),
|
||||
decoration: const BoxDecoration(color: Colors.white, border: Border(top: BorderSide(color: Color(0xFFEEEEEE)))),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.card,
|
||||
border: Border(top: BorderSide(color: theme.colorScheme.border)),
|
||||
),
|
||||
child: Row(children: [
|
||||
Stack(children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderRadius: BorderRadius.circular(AppTheme.rXs),
|
||||
child: Image.file(File(_pickedImagePath!), width: 60, height: 60, fit: BoxFit.cover),
|
||||
),
|
||||
Positioned(top: -4, right: -4, child: GestureDetector(
|
||||
onTap: () => setState(() => _pickedImagePath = null),
|
||||
child: Container(width: 20, height: 20, decoration: const BoxDecoration(color: Color(0xFF333333), shape: BoxShape.circle), child: const Icon(Icons.close, size: 14, color: Colors.white)),
|
||||
child: Container(
|
||||
width: 20, height: 20,
|
||||
decoration: const BoxDecoration(color: AppTheme.text, shape: BoxShape.circle),
|
||||
child: const Icon(Icons.close, size: 17, color: Colors.white),
|
||||
),
|
||||
)),
|
||||
]),
|
||||
const Spacer(),
|
||||
Text('点击发送上传图片', style: TextStyle(fontSize: 12, color: Colors.grey[500])),
|
||||
Text('点击发送上传图片', style: TextStyle(fontSize: 15, color: theme.colorScheme.mutedForeground)),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCompactInputBar(BuildContext context) {
|
||||
Widget _buildCompactInputBar(ShadThemeData theme) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
||||
decoration: BoxDecoration(color: Colors.white, border: Border(top: BorderSide(color: const Color(0xFFEEEEEE)))),
|
||||
child: Row(children: [
|
||||
IconButton(icon: const Icon(Icons.attach_file, size: 24, color: Color(0xFF666666)), onPressed: () => _showAttachmentPicker(context)),
|
||||
Expanded(child: TextField(
|
||||
controller: _textCtrl,
|
||||
style: const TextStyle(fontSize: 15),
|
||||
decoration: const InputDecoration(hintText: '输入你想说的...', hintStyle: TextStyle(fontSize: 15, color: Color(0xFFBBBBBB)), contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 10), border: InputBorder.none),
|
||||
onSubmitted: (_) => _sendMessage(),
|
||||
)),
|
||||
IconButton(icon: const Icon(Icons.send, size: 24, color: Color(0xFF8B9CF7)), onPressed: _sendMessage),
|
||||
padding: const EdgeInsets.symmetric(horizontal: AppTheme.sSm, vertical: AppTheme.sSm),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.card,
|
||||
border: Border(top: BorderSide(color: theme.colorScheme.border)),
|
||||
),
|
||||
child: Row(crossAxisAlignment: CrossAxisAlignment.end, children: [
|
||||
ShadButton.ghost(
|
||||
size: ShadButtonSize.sm,
|
||||
leading: Icon(LucideIcons.paperclip, size: 25, color: theme.colorScheme.mutedForeground),
|
||||
onPressed: () => _showAttachmentPicker(context, theme),
|
||||
),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _textCtrl,
|
||||
focusNode: _focusNode,
|
||||
style: TextStyle(fontSize: 18, color: theme.colorScheme.foreground),
|
||||
maxLines: null,
|
||||
textInputAction: TextInputAction.newline,
|
||||
decoration: InputDecoration(
|
||||
hintText: '输入你想说的...',
|
||||
hintStyle: TextStyle(fontSize: 18, color: theme.colorScheme.mutedForeground),
|
||||
filled: true,
|
||||
fillColor: theme.colorScheme.muted,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(AppTheme.rXl),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(AppTheme.rXl),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(AppTheme.rXl),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
),
|
||||
onSubmitted: (_) => _sendMessage(),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
ShadButton(
|
||||
size: ShadButtonSize.sm,
|
||||
leading: Icon(LucideIcons.send, size: 21, color: theme.colorScheme.primaryForeground),
|
||||
onPressed: _sendMessage,
|
||||
),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
void _showAttachmentPicker(BuildContext context, ShadThemeData theme) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: theme.colorScheme.card,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(AppTheme.rXl)),
|
||||
),
|
||||
builder: (ctx) => SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: AppTheme.sMd),
|
||||
child: Wrap(children: [
|
||||
ListTile(
|
||||
leading: Icon(LucideIcons.camera, color: theme.colorScheme.foreground),
|
||||
title: Text('拍照', style: TextStyle(color: theme.colorScheme.foreground)),
|
||||
onTap: () { Navigator.pop(ctx); _pickImage(ImageSource.camera); },
|
||||
),
|
||||
ListTile(
|
||||
leading: Icon(LucideIcons.image, color: theme.colorScheme.foreground),
|
||||
title: Text('从相册选', style: TextStyle(color: theme.colorScheme.foreground)),
|
||||
onTap: () { Navigator.pop(ctx); _pickImage(ImageSource.gallery); },
|
||||
),
|
||||
ListTile(
|
||||
leading: Icon(LucideIcons.file, color: theme.colorScheme.foreground),
|
||||
title: Text('传文件', style: TextStyle(color: theme.colorScheme.foreground)),
|
||||
onTap: () async {
|
||||
Navigator.pop(ctx);
|
||||
final result = await FilePicker.platform.pickFiles();
|
||||
if (result != null && result.files.isNotEmpty) {
|
||||
_textCtrl.text = '[文件已选择] ${result.files.first.name}';
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
},
|
||||
),
|
||||
]),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _pickImage(ImageSource source) async {
|
||||
final picker = ImagePicker();
|
||||
final picked = await picker.pickImage(source: source, imageQuality: 85);
|
||||
@@ -238,13 +317,4 @@ class _HomePageState extends ConsumerState<HomePage> {
|
||||
pushRoute(ref, 'dietCapture');
|
||||
}
|
||||
}
|
||||
|
||||
void _showAttachmentPicker(BuildContext context) {
|
||||
showModalBottomSheet(context: context, builder: (ctx) => SafeArea(child: Wrap(children: [
|
||||
ListTile(leading: const Icon(Icons.camera_alt), title: const Text('拍照'), onTap: () { Navigator.pop(ctx); _pickImage(ImageSource.camera); }),
|
||||
ListTile(leading: const Icon(Icons.photo_library), title: const Text('从相册选'), onTap: () { Navigator.pop(ctx); _pickImage(ImageSource.gallery); }),
|
||||
ListTile(leading: const Icon(Icons.attach_file), title: const Text('传文件'), onTap: () async { Navigator.pop(ctx); final result = await FilePicker.platform.pickFiles(); if (result != null && result.files.isNotEmpty) { _textCtrl.text = '[文件已选择] ${result.files.first.name}'; if (mounted) setState(() {}); }}),
|
||||
])));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user