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:
MingNian
2026-06-08 18:06:18 +08:00
parent 16d1d3d305
commit 58af5f6d5b
28 changed files with 1616 additions and 812 deletions

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import 'core/app_router.dart'; import 'core/app_router.dart';
import 'core/app_theme.dart'; import 'core/app_theme.dart';
import 'core/navigation_provider.dart'; import 'core/navigation_provider.dart';
@@ -26,6 +27,11 @@ class HealthApp extends ConsumerWidget {
], ],
locale: const Locale('zh'), locale: const Locale('zh'),
home: const _RootNavigator(), home: const _RootNavigator(),
// 注入 ShadTheme让所有页面都能用 shadcn 组件
builder: (context, child) => ShadTheme(
data: AppTheme.shadTheme,
child: child!,
),
); );
} }
} }

View File

@@ -1,97 +1,160 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
/// 健康管家 — Lavender Breeze 淡紫清风 /// 健康管家 —「朝露」Morning Dew
/// 深青绿 + 薄荷白,植物系清新风格
class AppTheme { class AppTheme {
AppTheme._(); AppTheme._();
// ── 颜色 ── // ── 主色调:深青绿 ──
static const Color primary = Color(0xFF6C5CE7); static const Color primary = Color(0xFF2D6A4F); // 深青绿
static const Color primaryLight = Color(0xFFEDEAFF); static const Color primaryLight = Color(0xFFD8F3DC); // 浅薄荷
static const Color primaryDark = Color(0xFF5A4BD1); static const Color primaryDark = Color(0xFF1B4332); // 深森林
static const Color bg = Color(0xFFF8F9FC); static const Color primaryGlow = Color(0xFFB7E4C7); // 发光薄荷
static const Color surface = Color(0xFFFFFFFF);
static const Color text = Color(0xFF2D2B32);
static const Color textSub = Color(0xFF8A8892);
static const Color textHint = Color(0xFFBFBCC4);
static const Color success = Color(0xFF6ECF8A);
static const Color error = Color(0xFFF56C6C);
static const Color warning = Color(0xFFF5A623);
static const Color accent = Color(0xFFFF8068);
static const Color border = Color(0xFFEAEAF0);
static const Color divider = Color(0xFFF2F2F6);
// ── 圆角 Token ── // ── 中性色 ──
static const double radiusXs = 8; static const Color bg = Color(0xFFF7FAF8); // 暖白底
static const double radiusSm = 12; static const Color surface = Color(0xFFFFFFFF);
static const double radiusMd = 16; static const Color text = Color(0xFF1B281F); // 深绿黑
static const double radiusLg = 20; static const Color textSub = Color(0xFF6B7B6F); // 灰绿
static const double radiusXl = 24; static const Color textHint = Color(0xFFB7BFB9); // 浅灰绿
static const double radiusPill = 999; static const Color border = Color(0xFFE0E8E2);
static const Color divider = Color(0xFFF0F4F1);
// ── 间距 Token ── // ── 语义色 ──
static const double spaceXs = 4; static const Color success = Color(0xFF40916C); // 翠绿
static const double spaceSm = 8; static const Color error = Color(0xFFE07A5F); // 陶土红
static const double spaceMd = 12; static const Color warning = Color(0xFFF2CC8F); // 蜂蜜黄
static const double spaceLg = 16; static const Color accent = Color(0xFF81B29A); // 中青绿
static const double spaceXl = 20; static const Color info = Color(0xFF52B788); // 亮绿
// ── 阴影 Token ── // ── 圆角 ──
static BoxShadow get shadowCard => BoxShadow(color: primary.withAlpha(15), blurRadius: 12, offset: const Offset(0, 4)); static const double rXs = 6;
static BoxShadow get shadowLight => BoxShadow(color: primary.withAlpha(10), blurRadius: 8, offset: const Offset(0, 2)); static const double rSm = 10;
static const double rMd = 14;
static const double rLg = 20;
static const double rXl = 28;
static const double rPill = 999;
/// 每个智能体的卡片色调 // ── 间距 ──
static const double sXs = 4;
static const double sSm = 8;
static const double sMd = 12;
static const double sLg = 16;
static const double sXl = 20;
static const double sXxl = 28;
// ── 阴影 ──
static BoxShadow get shadowCard =>
BoxShadow(color: primary.withAlpha(14), blurRadius: 20, offset: const Offset(0, 4));
static BoxShadow get shadowLight =>
BoxShadow(color: primary.withAlpha(8), blurRadius: 12, offset: const Offset(0, 2));
static BoxShadow get shadowElevated =>
BoxShadow(color: primary.withAlpha(20), blurRadius: 30, offset: const Offset(0, 8));
// ── 智能体配色 ──
static const Map<String, Color> agentColors = { static const Map<String, Color> agentColors = {
'default': Color(0xFFE8ECFF), // 淡蓝紫 'default': Color(0xFFD8F3DC), // 薄荷
'consultation': Color(0xFFE8F5FF), // 淡天蓝 'consultation': Color(0xFFE0F0FF), // 淡天蓝
'health': Color(0xFFE8FFF0), // 淡薄荷 'health': Color(0xFFE8FFE8), // 淡翠绿
'diet': Color(0xFFFFF2E8), // 淡杏 'diet': Color(0xFFFFF0E0), // 淡杏
'medication': Color(0xFFFFE8F0), // 淡粉 'medication': Color(0xFFFFE8EC), // 淡粉
'report': Color(0xFFE8F4FF), // 淡水蓝 'report': Color(0xFFE8F0FF), // 淡水蓝
'exercise': Color(0xFFF0E8FF), // 淡紫 'exercise': Color(0xFFF0E8FF), // 淡紫
}; };
static Color agentLight(String? name) => agentColors[name] ?? primaryLight; static Color agentLight(String? name) => agentColors[name] ?? primaryLight;
// ── Material 主题 ──
static ThemeData get lightTheme => ThemeData( static ThemeData get lightTheme => ThemeData(
useMaterial3: true, useMaterial3: true,
colorScheme: ColorScheme.fromSeed(seedColor: primary, primary: primary, surface: bg, brightness: Brightness.light), colorScheme: ColorScheme.fromSeed(
seedColor: primary,
primary: primary,
surface: bg,
brightness: Brightness.light,
),
scaffoldBackgroundColor: bg, scaffoldBackgroundColor: bg,
fontFamily: null, // 使用系统默认字体
appBarTheme: const AppBarTheme( appBarTheme: const AppBarTheme(
backgroundColor: surface, foregroundColor: text, elevation: 0, backgroundColor: surface,
centerTitle: true, scrolledUnderElevation: 0, foregroundColor: text,
titleTextStyle: TextStyle(fontSize: 17, fontWeight: FontWeight.w600, color: text), elevation: 0,
centerTitle: true,
scrolledUnderElevation: 0,
titleTextStyle: TextStyle(fontSize: 23, fontWeight: FontWeight.w600, color: text),
), ),
cardTheme: CardThemeData(color: surface, elevation: 0, cardTheme: CardThemeData(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(radiusXl)), margin: EdgeInsets.zero), color: surface,
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(rLg)),
margin: EdgeInsets.zero,
),
inputDecorationTheme: InputDecorationTheme( inputDecorationTheme: InputDecorationTheme(
filled: true, fillColor: const Color(0xFFF4F5FA), filled: true,
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), fillColor: bg,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(radiusMd), borderSide: BorderSide.none), contentPadding: const EdgeInsets.symmetric(horizontal: sLg, vertical: 16),
enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(radiusMd), borderSide: BorderSide.none), border: OutlineInputBorder(borderRadius: BorderRadius.circular(rMd), borderSide: BorderSide.none),
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(radiusMd), borderSide: const BorderSide(color: primary, width: 1.5)), enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(rMd), borderSide: BorderSide.none),
hintStyle: const TextStyle(color: textHint, fontSize: 15), focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(rMd),
borderSide: const BorderSide(color: primary, width: 2),
),
hintStyle: const TextStyle(color: textHint, fontSize: 20),
), ),
elevatedButtonTheme: ElevatedButtonThemeData(style: ElevatedButton.styleFrom( elevatedButtonTheme: ElevatedButtonThemeData(
backgroundColor: primary, foregroundColor: Colors.white, style: ElevatedButton.styleFrom(
minimumSize: const Size(double.infinity, 52), backgroundColor: primary,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(radiusLg)), foregroundColor: Colors.white,
textStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600), elevation: 0, minimumSize: const Size(double.infinity, 56),
)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(rPill)),
textStyle: const TextStyle(fontSize: 21, fontWeight: FontWeight.w600),
elevation: 0,
),
),
dialogTheme: DialogThemeData(shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(radiusXl))), dialogTheme: DialogThemeData(shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(rXl))),
textTheme: const TextTheme( textTheme: const TextTheme(
headlineLarge: TextStyle(fontSize: 24, fontWeight: FontWeight.w700, color: text), headlineLarge: TextStyle(fontSize: 32, fontWeight: FontWeight.w700, color: text, letterSpacing: -0.5),
titleLarge: TextStyle(fontSize: 20, fontWeight: FontWeight.w600, color: text), titleLarge: TextStyle(fontSize: 23, fontWeight: FontWeight.w600, color: text),
titleMedium: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: text), titleMedium: TextStyle(fontSize: 21, fontWeight: FontWeight.w600, color: text),
bodyLarge: TextStyle(fontSize: 16, color: text, height: 1.5), bodyLarge: TextStyle(fontSize: 21, color: text, height: 1.5),
bodyMedium: TextStyle(fontSize: 15, color: textSub, height: 1.4), bodyMedium: TextStyle(fontSize: 20, color: textSub, height: 1.4),
labelMedium: TextStyle(fontSize: 13, color: textSub), labelMedium: TextStyle(fontSize: 18, color: textSub),
labelSmall: TextStyle(fontSize: 11, color: textHint), labelSmall: TextStyle(fontSize: 16, color: textHint),
), ),
); );
// ── Shadcn UI 主题 ──
static ShadThemeData get shadTheme => ShadThemeData(
brightness: Brightness.light,
colorScheme: const ShadVioletColorScheme.light(
background: bg,
foreground: text,
card: surface,
cardForeground: text,
popover: surface,
popoverForeground: text,
primary: primary,
primaryForeground: Colors.white,
secondary: Color(0xFFF0F4F1),
secondaryForeground: text,
muted: Color(0xFFF0F4F1),
mutedForeground: textSub,
accent: accent,
accentForeground: Colors.white,
destructive: error,
destructiveForeground: Colors.white,
border: border,
input: border,
ring: primary,
selection: Color(0xFFB7E4C7),
),
radius: BorderRadius.circular(rMd),
);
} }

View File

@@ -1,11 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import '../../core/app_theme.dart';
import '../../core/navigation_provider.dart'; import '../../core/navigation_provider.dart';
import '../../providers/auth_provider.dart'; import '../../providers/auth_provider.dart';
class LoginPage extends ConsumerStatefulWidget { class LoginPage extends ConsumerStatefulWidget {
const LoginPage({super.key}); const LoginPage({super.key});
@override ConsumerState<LoginPage> createState() => _LoginPageState(); @override ConsumerState<LoginPage> createState() => _LoginPageState();
} }
@@ -32,54 +33,216 @@ class _LoginPageState extends ConsumerState<LoginPage> {
} }
void _startCountdown() async { void _startCountdown() async {
for (var i = 60; i > 0; i--) { await Future.delayed(const Duration(seconds: 1)); if (!mounted) return; setState(() => _countdown = i - 1); } for (var i = 60; i > 0; i--) {
await Future.delayed(const Duration(seconds: 1));
if (!mounted) return;
setState(() => _countdown = i - 1);
}
} }
Future<void> _login() async { Future<void> _login() async {
if (!_agreed) { setState(() => _error = '请阅读并同意服务协议和隐私政策'); return; } if (!_agreed) { setState(() => _error = '请阅读并同意服务协议和隐私政策'); return; }
setState(() { _loading = true; _error = null; }); setState(() { _loading = true; _error = null; });
final err = await ref.read(authProvider.notifier).login(_phoneCtrl.text.trim(), _codeCtrl.text.trim()); final err = await ref.read(authProvider.notifier).login(_phoneCtrl.text.trim(), _codeCtrl.text.trim());
setState(() => _loading = false ); setState(() => _loading = false);
if (err != null) { setState(() => _error = err); return; } if (err != null) { setState(() => _error = err); return; }
goRoute(ref, 'home'); goRoute(ref, 'home');
} }
@override Widget build(BuildContext context) { @override Widget build(BuildContext context) {
final authState = ref.watch(authProvider); final authState = ref.watch(authProvider);
if (authState.isLoggedIn && !authState.isLoading) WidgetsBinding.instance.addPostFrameCallback((_) => goRoute(ref, 'home')); final theme = ShadTheme.of(context);
if (authState.isLoggedIn && !authState.isLoading) {
WidgetsBinding.instance.addPostFrameCallback((_) => goRoute(ref, 'home'));
}
return Scaffold( return Scaffold(
body: Container( body: Container(
decoration: const BoxDecoration(gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [Color(0xFFF0F2FF), Color(0xFFF0F2FF), Color(0xFFE8E4FF)])), decoration: const BoxDecoration(
child: SafeArea(child: Center(child: Padding(padding: const EdgeInsets.symmetric(horizontal: 32), child: Column(mainAxisSize: MainAxisSize.min, children: [ gradient: LinearGradient(
const SizedBox(height: 24), begin: Alignment.topLeft,
Container(width: 100, height: 100, decoration: BoxDecoration(color: const Color(0xFF6C5CE7).withAlpha(20), borderRadius: BorderRadius.circular(50)), child: Stack(alignment: Alignment.center, children: [ end: Alignment.bottomRight,
Container(width: 72, height: 72, decoration: BoxDecoration(color: Colors.white.withAlpha(200), borderRadius: BorderRadius.circular(36)), child: const Icon(Icons.favorite, size: 36, color: Color(0xFF6C5CE7))), colors: [AppTheme.bg, AppTheme.bg, AppTheme.primaryLight],
])), ),
const SizedBox(height: 20), ),
const Text('健康管家', style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Color(0xFF1A1A1A))), child: SafeArea(child: Center(child: SingleChildScrollView(
const SizedBox(height: 8), padding: const EdgeInsets.symmetric(horizontal: 32),
Text('你的 AI 心脏健康管家', style: TextStyle(fontSize: 15, color: Colors.grey[500])), child: Column(mainAxisSize: MainAxisSize.min, children: [
const SizedBox(height: 36), const SizedBox(height: 24),
TextField(controller: _phoneCtrl, keyboardType: TextInputType.phone, maxLength: 11, // Logo
decoration: InputDecoration(hintText: '请输入手机号', prefixIcon: const Padding(padding: EdgeInsets.only(left: 12), child: Text('+86', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500))), counterText: '', filled: true, fillColor: Colors.white, border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none), enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none), focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: Color(0xFF6C5CE7), width: 1.5)))), Container(
const SizedBox(height: 16), width: 100, height: 100,
Row(children: [ decoration: BoxDecoration(
Expanded(child: TextField(controller: _codeCtrl, keyboardType: TextInputType.number, maxLength: 6, color: AppTheme.primary.withAlpha(25),
decoration: InputDecoration(hintText: '验证码', filled: true, fillColor: Colors.white, border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none), enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none), focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: Color(0xFF6C5CE7), width: 1.5)), counterText: ''))), borderRadius: BorderRadius.circular(50),
const SizedBox(width: 12), ),
GestureDetector(onTap: (_countdown > 0 || _sending) ? null : _sendSms, child: Container(width: 100, height: 48, alignment: Alignment.center, decoration: BoxDecoration(color: _countdown > 0 ? Colors.grey[300] : const Color(0xFF6C5CE7), borderRadius: BorderRadius.circular(12)), child: Text(_sending ? '发送中' : _countdown > 0 ? '${_countdown}s' : '获取验证码', style: TextStyle(fontSize: 14, color: _countdown > 0 ? Colors.grey[600] : Colors.white, fontWeight: FontWeight.w500)))), child: Stack(alignment: Alignment.center, children: [
Container(
width: 72, height: 72,
decoration: BoxDecoration(
color: Colors.white.withAlpha(200),
borderRadius: BorderRadius.circular(36),
),
child: const Icon(LucideIcons.heart, size: 36, color: AppTheme.primary),
),
]),
),
const SizedBox(height: 20),
Text('健康管家', style: TextStyle(fontSize: 32, fontWeight: FontWeight.bold, color: theme.colorScheme.foreground)),
const SizedBox(height: 8),
Text('你的 AI 心脏健康管家', style: TextStyle(fontSize: 18, color: theme.colorScheme.mutedForeground)),
const SizedBox(height: 36),
// 手机号输入
_buildPhoneInput(theme),
const SizedBox(height: AppTheme.sLg),
// 验证码输入
_buildCodeRow(theme),
const SizedBox(height: AppTheme.sSm),
// 协议勾选
_buildAgreement(theme),
// 错误提示
if (_error != null)
Padding(padding: const EdgeInsets.only(top: AppTheme.sMd), child: Text(_error!, style: TextStyle(color: theme.colorScheme.destructive, fontSize: 16))),
const SizedBox(height: AppTheme.rXl),
// 登录按钮
_buildLoginButton(theme),
const SizedBox(height: AppTheme.sXl),
]), ]),
const SizedBox(height: 8), ))),
Align(alignment: Alignment.centerLeft, child: GestureDetector(onTap: () => setState(() => _agreed = !_agreed), child: Row(mainAxisSize: MainAxisSize.min, children: [ ),
Container(width: 20, height: 20, margin: const EdgeInsets.only(right: 6), decoration: BoxDecoration(shape: BoxShape.rectangle, color: _agreed ? const Color(0xFF6C5CE7) : Colors.transparent, border: Border.all(color: _agreed ? const Color(0xFF6C5CE7) : const Color(0xFFBDBDBD), width: 1.5), borderRadius: BorderRadius.circular(4)), child: _agreed ? const Icon(Icons.check, size: 14, color: Colors.white) : null), );
RichText(text: TextSpan(children: [TextSpan(text: '已阅读并同意', style: TextStyle(fontSize: 13, color: Colors.grey[600])), TextSpan(text: '《服务协议》', style: const TextStyle(fontSize: 13, color: Color(0xFF6C5CE7))), TextSpan(text: '', style: TextStyle(fontSize: 13, color: Colors.grey[600])), TextSpan(text: '《隐私政策》', style: const TextStyle(fontSize: 13, color: Color(0xFF6C5CE7)))])), }
]))),
if (_error != null) Padding(padding: const EdgeInsets.only(top: 12), child: Text(_error!, style: const TextStyle(color: Color(0xFFE53935), fontSize: 13))), Widget _buildPhoneInput(ShadThemeData theme) {
const SizedBox(height: 24), return Container(
GestureDetector(onTap: _loading ? null : _login, child: Container(width: double.infinity, height: 50, alignment: Alignment.center, decoration: BoxDecoration(gradient: const LinearGradient(colors: [Color(0xFFA8B5FA), Color(0xFF6C5CE7)]), borderRadius: BorderRadius.circular(25), boxShadow: [BoxShadow(color: const Color(0xFF6C5CE7).withAlpha(80), blurRadius: 16, offset: const Offset(0, 8))]), child: _loading ? const SizedBox(width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2.5, color: Colors.white)) : const Text('登 录', style: TextStyle(fontSize: 17, color: Colors.white, fontWeight: FontWeight.w600, letterSpacing: 2)))), decoration: BoxDecoration(
const SizedBox(height: 20), color: theme.colorScheme.card,
])))), borderRadius: BorderRadius.circular(AppTheme.rSm),
boxShadow: [AppTheme.shadowLight],
),
child: Row(children: [
const Padding(padding: EdgeInsets.only(left: AppTheme.sLg), child: Text('+86', style: TextStyle(fontSize: 19, fontWeight: FontWeight.w500, color: AppTheme.text))),
Container(width: 1, height: 24, color: theme.colorScheme.border, margin: const EdgeInsets.symmetric(horizontal: AppTheme.sMd)),
Expanded(
child: TextField(
controller: _phoneCtrl,
keyboardType: TextInputType.phone,
maxLength: 11,
style: TextStyle(fontSize: 19, color: theme.colorScheme.foreground),
decoration: InputDecoration(
hintText: '请输入手机号',
hintStyle: TextStyle(color: theme.colorScheme.mutedForeground),
border: InputBorder.none,
counterText: '',
contentPadding: const EdgeInsets.symmetric(vertical: AppTheme.sLg),
),
),
),
]),
);
}
Widget _buildCodeRow(ShadThemeData theme) {
final codeDisabled = _countdown > 0 || _sending;
return Row(children: [
Expanded(
child: Container(
decoration: BoxDecoration(
color: theme.colorScheme.card,
borderRadius: BorderRadius.circular(AppTheme.rSm),
boxShadow: [AppTheme.shadowLight],
),
child: TextField(
controller: _codeCtrl,
keyboardType: TextInputType.number,
maxLength: 6,
style: TextStyle(fontSize: 19, color: theme.colorScheme.foreground),
decoration: InputDecoration(
hintText: '验证码',
hintStyle: TextStyle(color: theme.colorScheme.mutedForeground),
border: InputBorder.none,
counterText: '',
contentPadding: const EdgeInsets.symmetric(horizontal: AppTheme.sLg, vertical: AppTheme.sLg),
),
),
),
),
const SizedBox(width: AppTheme.sMd),
GestureDetector(
onTap: codeDisabled ? null : _sendSms,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
width: 110, height: 52,
alignment: Alignment.center,
decoration: BoxDecoration(
color: codeDisabled ? theme.colorScheme.muted : AppTheme.primary,
borderRadius: BorderRadius.circular(AppTheme.rSm),
),
child: Text(
_sending ? '发送中' : _countdown > 0 ? '${_countdown}s' : '获取验证码',
style: TextStyle(
fontSize: 17,
color: codeDisabled ? theme.colorScheme.mutedForeground : Colors.white,
fontWeight: FontWeight.w500,
),
),
),
),
]);
}
Widget _buildAgreement(ShadThemeData theme) {
return Align(
alignment: Alignment.centerLeft,
child: GestureDetector(
onTap: () => setState(() => _agreed = !_agreed),
child: Row(mainAxisSize: MainAxisSize.min, children: [
Container(
width: 18, height: 18,
margin: const EdgeInsets.only(right: 6),
decoration: BoxDecoration(
color: _agreed ? AppTheme.primary : Colors.transparent,
borderRadius: BorderRadius.circular(4),
border: Border.all(
color: _agreed ? AppTheme.primary : theme.colorScheme.border,
width: 1.5,
),
),
child: _agreed ? Icon(LucideIcons.check, size: 15, color: theme.colorScheme.primaryForeground) : null,
),
RichText(text: TextSpan(children: [
TextSpan(text: '已阅读并同意', style: TextStyle(fontSize: 16, color: theme.colorScheme.mutedForeground)),
TextSpan(text: '《服务协议》', style: const TextStyle(fontSize: 16, color: AppTheme.primary)),
TextSpan(text: '', style: TextStyle(fontSize: 16, color: theme.colorScheme.mutedForeground)),
TextSpan(text: '《隐私政策》', style: const TextStyle(fontSize: 16, color: AppTheme.primary)),
])),
]),
),
);
}
Widget _buildLoginButton(ShadThemeData theme) {
return GestureDetector(
onTap: _loading ? null : _login,
child: Container(
width: double.infinity,
height: 52,
alignment: Alignment.center,
decoration: BoxDecoration(
gradient: const LinearGradient(colors: [Color(0xFF9B8FEF), AppTheme.primary]),
borderRadius: BorderRadius.circular(AppTheme.rPill),
boxShadow: [BoxShadow(color: AppTheme.primary.withAlpha(80), blurRadius: 16, offset: const Offset(0, 8))],
),
child: _loading
? const SizedBox(width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2.5, color: Colors.white))
: const Text('登 录', style: TextStyle(fontSize: 20, color: Colors.white, fontWeight: FontWeight.w600, letterSpacing: 2)),
), ),
); );
} }

View File

@@ -1,6 +1,7 @@
import 'package:fl_chart/fl_chart.dart'; import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/app_theme.dart';
import '../../providers/auth_provider.dart'; import '../../providers/auth_provider.dart';
import '../../providers/data_providers.dart'; import '../../providers/data_providers.dart';
@@ -109,7 +110,7 @@ class _TrendPageState extends ConsumerState<TrendPage> {
builder: (ctx) => Padding( builder: (ctx) => Padding(
padding: EdgeInsets.only(bottom: MediaQuery.of(ctx).viewInsets.bottom, left: 20, right: 20, top: 20), padding: EdgeInsets.only(bottom: MediaQuery.of(ctx).viewInsets.bottom, left: 20, right: 20, top: 20),
child: Column(mainAxisSize: MainAxisSize.min, children: [ child: Column(mainAxisSize: MainAxisSize.min, children: [
Text('录入$_name', style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w700)), Text('录入$_name', style: const TextStyle(fontSize: 21, fontWeight: FontWeight.w700)),
const SizedBox(height: 16), const SizedBox(height: 16),
if (_isBP) ...[ if (_isBP) ...[
Row(children: [ Row(children: [
@@ -155,7 +156,7 @@ class _TrendPageState extends ConsumerState<TrendPage> {
}, },
style: ElevatedButton.styleFrom(backgroundColor: _color, foregroundColor: Colors.white, style: ElevatedButton.styleFrom(backgroundColor: _color, foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), padding: const EdgeInsets.symmetric(vertical: 14)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), padding: const EdgeInsets.symmetric(vertical: 14)),
child: const Text('确认录入', style: TextStyle(fontSize: 16)), child: const Text('确认录入', style: TextStyle(fontSize: 19)),
)), )),
const SizedBox(height: 20), const SizedBox(height: 20),
]), ]),
@@ -165,7 +166,7 @@ class _TrendPageState extends ConsumerState<TrendPage> {
@override Widget build(BuildContext context) { @override Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFF8F9FC), backgroundColor: AppTheme.bg,
appBar: AppBar(title: const Text('健康概览'), centerTitle: true), appBar: AppBar(title: const Text('健康概览'), centerTitle: true),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
onPressed: _showAddDialog, onPressed: _showAddDialog,
@@ -173,7 +174,7 @@ class _TrendPageState extends ConsumerState<TrendPage> {
child: const Icon(Icons.add), child: const Icon(Icons.add),
), ),
body: _loading body: _loading
? const Center(child: CircularProgressIndicator(color: Color(0xFF6C5CE7))) ? const Center(child: CircularProgressIndicator(color: AppTheme.primary))
: RefreshIndicator( : RefreshIndicator(
onRefresh: _loadAll, onRefresh: _loadAll,
child: SingleChildScrollView( child: SingleChildScrollView(
@@ -211,7 +212,7 @@ class _TrendPageState extends ConsumerState<TrendPage> {
border: Border.all(color: sel ? color : const Color(0xFFE0E0E0)), border: Border.all(color: sel ? color : const Color(0xFFE0E0E0)),
), ),
child: Text(m['label'] as String, style: TextStyle( child: Text(m['label'] as String, style: TextStyle(
fontSize: 14, fontWeight: FontWeight.w600, fontSize: 17, fontWeight: FontWeight.w600,
color: sel ? Colors.white : const Color(0xFF666666)), color: sel ? Colors.white : const Color(0xFF666666)),
), ),
), ),
@@ -230,7 +231,7 @@ class _TrendPageState extends ConsumerState<TrendPage> {
const SizedBox(height: 8), const SizedBox(height: 8),
Text('暂无$_name数据', style: const TextStyle(color: Color(0xFFBBBBBB))), Text('暂无$_name数据', style: const TextStyle(color: Color(0xFFBBBBBB))),
const SizedBox(height: 4), const SizedBox(height: 4),
const Text('点击右下角 + 手动录入', style: TextStyle(fontSize: 12, color: Color(0xFFCCCCCC))), const Text('点击右下角 + 手动录入', style: TextStyle(fontSize: 15, color: Color(0xFFCCCCCC))),
])), ])),
); );
} }
@@ -258,11 +259,11 @@ class _TrendPageState extends ConsumerState<TrendPage> {
child: Column(children: [ child: Column(children: [
Row(children: [ Row(children: [
const SizedBox(width: 8), const SizedBox(width: 8),
Text(_name, style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: _color)), Text(_name, style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: _color)),
const SizedBox(width: 6), const SizedBox(width: 6),
Text('趋势 (${_filtered.length}条)', style: const TextStyle(fontSize: 12, color: Color(0xFF999999))), Text('趋势 (${_filtered.length}条)', style: const TextStyle(fontSize: 15, color: Color(0xFF999999))),
const Spacer(), const Spacer(),
Text('单位: $_unit', style: const TextStyle(fontSize: 11, color: Color(0xFFBBBBBB))), Text('单位: $_unit', style: const TextStyle(fontSize: 14, color: Color(0xFFBBBBBB))),
]), ]),
const SizedBox(height: 10), const SizedBox(height: 10),
SizedBox( SizedBox(
@@ -280,7 +281,7 @@ class _TrendPageState extends ConsumerState<TrendPage> {
leftTitles: AxisTitles(sideTitles: SideTitles( leftTitles: AxisTitles(sideTitles: SideTitles(
showTitles: true, reservedSize: 40, showTitles: true, reservedSize: 40,
getTitlesWidget: (v, meta) => Text(v.toStringAsFixed(v.truncateToDouble() == v ? 0 : 1), getTitlesWidget: (v, meta) => Text(v.toStringAsFixed(v.truncateToDouble() == v ? 0 : 1),
style: const TextStyle(fontSize: 10, color: Color(0xFF999999))), style: const TextStyle(fontSize: 13, color: Color(0xFF999999))),
)), )),
bottomTitles: AxisTitles(sideTitles: SideTitles( bottomTitles: AxisTitles(sideTitles: SideTitles(
showTitles: true, reservedSize: 24, showTitles: true, reservedSize: 24,
@@ -289,7 +290,7 @@ class _TrendPageState extends ConsumerState<TrendPage> {
final idx = v.toInt(); final idx = v.toInt();
if (idx < 0 || idx >= _filtered.length) return const SizedBox.shrink(); if (idx < 0 || idx >= _filtered.length) return const SizedBox.shrink();
final d = _filtered[idx]['date'] as DateTime; final d = _filtered[idx]['date'] as DateTime;
return Text('${d.month}/${d.day}', style: const TextStyle(fontSize: 9, color: Color(0xFFBBBBBB))); return Text('${d.month}/${d.day}', style: const TextStyle(fontSize: 12, color: Color(0xFFBBBBBB)));
}, },
)), )),
topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
@@ -321,7 +322,7 @@ class _TrendPageState extends ConsumerState<TrendPage> {
final v = _isBP ? '${r['systolic']}/${r['diastolic']}' : '${r['value']}'; final v = _isBP ? '${r['systolic']}/${r['diastolic']}' : '${r['value']}';
return LineTooltipItem( return LineTooltipItem(
'${d.month}/${d.day} ${d.hour}:${d.minute.toString().padLeft(2, '0')}\n$v $_unit', '${d.month}/${d.day} ${d.hour}:${d.minute.toString().padLeft(2, '0')}\n$v $_unit',
TextStyle(color: _color, fontSize: 12, fontWeight: FontWeight.w600), TextStyle(color: _color, fontSize: 15, fontWeight: FontWeight.w600),
); );
}).toList(), }).toList(),
), ),
@@ -338,9 +339,9 @@ class _TrendPageState extends ConsumerState<TrendPage> {
if (_filtered.isEmpty) return const SizedBox.shrink(); if (_filtered.isEmpty) return const SizedBox.shrink();
return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Row(children: [ Row(children: [
const Text('历史记录', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w700)), const Text('历史记录', style: TextStyle(fontSize: 19, fontWeight: FontWeight.w700)),
const Spacer(), const Spacer(),
Text('${_filtered.length}', style: const TextStyle(fontSize: 13, color: Color(0xFF999999))), Text('${_filtered.length}', style: const TextStyle(fontSize: 16, color: Color(0xFF999999))),
]), ]),
const SizedBox(height: 10), const SizedBox(height: 10),
..._filtered.reversed.take(30).map((r) { ..._filtered.reversed.take(30).map((r) {
@@ -359,20 +360,20 @@ class _TrendPageState extends ConsumerState<TrendPage> {
child: Row(children: [ child: Row(children: [
Container(width: 40, height: 40, decoration: BoxDecoration( Container(width: 40, height: 40, decoration: BoxDecoration(
color: _color.withAlpha(20), borderRadius: BorderRadius.circular(10)), color: _color.withAlpha(20), borderRadius: BorderRadius.circular(10)),
child: Center(child: Text(_getMetricIcon(_selected), style: const TextStyle(fontSize: 18))), child: Center(child: Text(_getMetricIcon(_selected), style: const TextStyle(fontSize: 21))),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text('${date.month}${date.day}${date.hour}:${date.minute.toString().padLeft(2, '0')}', Text('${date.month}${date.day}${date.hour}:${date.minute.toString().padLeft(2, '0')}',
style: const TextStyle(fontSize: 12, color: Color(0xFF999999))), style: const TextStyle(fontSize: 15, color: Color(0xFF999999))),
const SizedBox(height: 2), const SizedBox(height: 2),
Text('$display $_unit', style: TextStyle(fontSize: 20, fontWeight: FontWeight.w700, Text('$display $_unit', style: TextStyle(fontSize: 23, fontWeight: FontWeight.w700,
color: abnormal ? const Color(0xFFEF4444) : const Color(0xFF1A1A1A))), color: abnormal ? const Color(0xFFEF4444) : const Color(0xFF1A1A1A))),
])), ])),
if (abnormal) if (abnormal)
Container(padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), Container(padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(color: const Color(0xFFFEE2E2), borderRadius: BorderRadius.circular(6)), decoration: BoxDecoration(color: const Color(0xFFFEE2E2), borderRadius: BorderRadius.circular(6)),
child: const Text('异常', style: TextStyle(fontSize: 11, color: Color(0xFFEF4444), fontWeight: FontWeight.w500))), child: const Text('异常', style: TextStyle(fontSize: 14, color: Color(0xFFEF4444), fontWeight: FontWeight.w500))),
]), ]),
); );
}), }),

View File

@@ -1,6 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import '../../core/app_theme.dart';
import '../../core/navigation_provider.dart'; import '../../core/navigation_provider.dart';
import '../../providers/data_providers.dart'; import '../../providers/data_providers.dart';
import '../../providers/consultation_provider.dart'; import '../../providers/consultation_provider.dart';
@@ -42,7 +44,7 @@ class DoctorListPage extends ConsumerWidget {
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
boxShadow: [ boxShadow: [
BoxShadow(color: const Color(0xFF8B9CF7).withAlpha(15), blurRadius: 12, offset: const Offset(0, 4)) BoxShadow(color: AppTheme.primary.withAlpha(15), blurRadius: 12, offset: const Offset(0, 4))
], ],
), ),
child: Padding( child: Padding(
@@ -52,26 +54,26 @@ class DoctorListPage extends ConsumerWidget {
children: [ children: [
CircleAvatar( CircleAvatar(
radius: 28, radius: 28,
backgroundColor: const Color(0xFFF0F2FF), backgroundColor: AppTheme.primaryLight,
child: Text( child: Text(
(d['name'] as String?)?.isNotEmpty == true ? d['name']![0] : '?', (d['name'] as String?)?.isNotEmpty == true ? d['name']![0] : '?',
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.w600, color: Color(0xFF8B9CF7)), style: const TextStyle(fontSize: 25, fontWeight: FontWeight.w600, color: AppTheme.primaryLight),
), ),
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFF0F2FF), color: AppTheme.primaryLight,
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(6),
), ),
child: Text(d['title'] ?? '', style: const TextStyle(fontSize: 12, color: Color(0xFF8B9CF7), fontWeight: FontWeight.w500)), child: Text(d['title'] ?? '', style: const TextStyle(fontSize: 15, color: AppTheme.primaryLight, fontWeight: FontWeight.w500)),
), ),
const SizedBox(height: 6), const SizedBox(height: 6),
Text(d['department'] ?? '', style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFF333333))), Text(d['department'] ?? '', style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w600, color: Color(0xFF333333))),
const SizedBox(height: 8), const SizedBox(height: 8),
Text(d['introduction'] ?? '', Text(d['introduction'] ?? '',
style: const TextStyle(fontSize: 12, color: Color(0xFF888888), height: 1.4), style: const TextStyle(fontSize: 15, color: Color(0xFF888888), height: 1.4),
maxLines: 4, maxLines: 4,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center), textAlign: TextAlign.center),
@@ -82,12 +84,12 @@ class DoctorListPage extends ConsumerWidget {
child: ElevatedButton( child: ElevatedButton(
onPressed: () => pushRoute(ref, 'consultation', params: {'id': d['id']?.toString() ?? ''}), onPressed: () => pushRoute(ref, 'consultation', params: {'id': d['id']?.toString() ?? ''}),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF8B9CF7), backgroundColor: AppTheme.primary,
foregroundColor: Colors.white, foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
), ),
child: const Text('立即咨询', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600)), child: const Text('立即咨询', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
), ),
), ),
], ],
@@ -164,14 +166,14 @@ class _DoctorChatPageState extends ConsumerState<DoctorChatPage> {
final canSend = state.status == 'AiTalking' && !state.isSending; final canSend = state.status == 'AiTalking' && !state.isSending;
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFF8F9FC), backgroundColor: AppTheme.bg,
appBar: AppBar( appBar: AppBar(
backgroundColor: Colors.white, backgroundColor: Colors.white,
title: Column(children: [ title: Column(children: [
Text(state.doctorName.isNotEmpty ? '${state.doctorName} · ${state.doctorDepartment}' : '问诊对话', Text(state.doctorName.isNotEmpty ? '${state.doctorName} · ${state.doctorDepartment}' : '问诊对话',
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))), style: const TextStyle(fontSize: 19, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
if (state.doctorName.isNotEmpty) if (state.doctorName.isNotEmpty)
Text(_statusText(state.status), style: const TextStyle(fontSize: 12, color: Color(0xFF8B9CF7))), Text(_statusText(state.status), style: const TextStyle(fontSize: 15, color: AppTheme.primaryLight)),
]), ]),
centerTitle: true, centerTitle: true,
actions: [ actions: [
@@ -179,16 +181,16 @@ class _DoctorChatPageState extends ConsumerState<DoctorChatPage> {
margin: const EdgeInsets.only(right: 12), margin: const EdgeInsets.only(right: 12),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFF0F2FF), color: AppTheme.primaryLight,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
child: Text('剩余${state.quotaRemaining}/${state.quotaTotal}', child: Text('剩余${state.quotaRemaining}/${state.quotaTotal}',
style: const TextStyle(fontSize: 12, color: Color(0xFF8B9CF7), fontWeight: FontWeight.w500)), style: const TextStyle(fontSize: 15, color: AppTheme.primaryLight, fontWeight: FontWeight.w500)),
), ),
], ],
), ),
body: state.isLoading body: state.isLoading
? const Center(child: CircularProgressIndicator(color: Color(0xFF8B9CF7))) ? const Center(child: CircularProgressIndicator(color: AppTheme.primaryLight))
: Column(children: [ : Column(children: [
Expanded(child: _buildMessageList(state)), Expanded(child: _buildMessageList(state)),
_buildInputBar(canSend), _buildInputBar(canSend),
@@ -220,7 +222,7 @@ class _DoctorChatPageState extends ConsumerState<DoctorChatPage> {
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.82), constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.82),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isUser ? const Color(0xFF8B9CF7) : const Color(0xFFFEFEFF), color: isUser ? AppTheme.primary : const Color(0xFFFEFEFF),
borderRadius: BorderRadius.only( borderRadius: BorderRadius.only(
topLeft: Radius.circular(isUser ? 20 : 4), topLeft: Radius.circular(isUser ? 20 : 4),
topRight: Radius.circular(isUser ? 4 : 20), topRight: Radius.circular(isUser ? 4 : 20),
@@ -230,26 +232,26 @@ class _DoctorChatPageState extends ConsumerState<DoctorChatPage> {
border: isUser ? null : Border.all(color: const Color(0xFFD8DCFD), width: 1.5), border: isUser ? null : Border.all(color: const Color(0xFFD8DCFD), width: 1.5),
boxShadow: isUser boxShadow: isUser
? [] ? []
: [BoxShadow(color: const Color(0xFF8B9CF7).withAlpha(12), blurRadius: 10, offset: const Offset(0, 3))], : [BoxShadow(color: AppTheme.primary.withAlpha(12), blurRadius: 10, offset: const Offset(0, 3))],
), ),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
if (!isUser && msg.senderName != null) ...[ if (!isUser && msg.senderName != null) ...[
Row(children: [ Row(children: [
const CircleAvatar(radius: 10, backgroundColor: Color(0xFFF0F2FF), const CircleAvatar(radius: 10, backgroundColor: Color(0xFFF0F2FF),
child: Icon(Icons.smart_toy, size: 12, color: Color(0xFF8B9CF7))), child: Icon(Icons.smart_toy, size: 15, color: AppTheme.primaryLight)),
const SizedBox(width: 6), const SizedBox(width: 6),
Text(msg.senderName!, style: const TextStyle(fontSize: 12, color: Color(0xFF9E9E9E))), Text(msg.senderName!, style: const TextStyle(fontSize: 15, color: Color(0xFF9E9E9E))),
]), ]),
const SizedBox(height: 8), const SizedBox(height: 8),
], ],
if (isUser) if (isUser)
Text(msg.content, style: const TextStyle(fontSize: 16, color: Colors.white, height: 1.4)) Text(msg.content, style: const TextStyle(fontSize: 19, color: Colors.white, height: 1.4))
else else
MarkdownBody( MarkdownBody(
data: msg.content, data: msg.content,
selectable: true, selectable: true,
styleSheet: MarkdownStyleSheet( styleSheet: MarkdownStyleSheet(
p: const TextStyle(fontSize: 16, color: Color(0xFF1A1A1A), height: 1.5), p: const TextStyle(fontSize: 19, color: Color(0xFF1A1A1A), height: 1.5),
), ),
), ),
if (isAi) ...[ if (isAi) ...[
@@ -261,7 +263,7 @@ class _DoctorChatPageState extends ConsumerState<DoctorChatPage> {
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(6),
), ),
child: Text('🏷️ 以上为AI分析具体请咨询${state.doctorName}', child: Text('🏷️ 以上为AI分析具体请咨询${state.doctorName}',
style: const TextStyle(fontSize: 10, color: Color(0xFFF9A825))), style: const TextStyle(fontSize: 13, color: Color(0xFFF9A825))),
), ),
], ],
]), ]),
@@ -281,10 +283,10 @@ class _DoctorChatPageState extends ConsumerState<DoctorChatPage> {
child: TextField( child: TextField(
controller: _textCtrl, controller: _textCtrl,
enabled: canSend, enabled: canSend,
style: const TextStyle(fontSize: 15), style: const TextStyle(fontSize: 18),
decoration: InputDecoration( decoration: InputDecoration(
hintText: canSend ? '描述您的症状...' : '对话已结束', hintText: canSend ? '描述您的症状...' : '对话已结束',
hintStyle: const TextStyle(fontSize: 15, color: Color(0xFFBBBBBB)), hintStyle: const TextStyle(fontSize: 18, color: Color(0xFFBBBBBB)),
contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
border: InputBorder.none, border: InputBorder.none,
), ),
@@ -292,8 +294,8 @@ class _DoctorChatPageState extends ConsumerState<DoctorChatPage> {
), ),
), ),
IconButton( IconButton(
icon: Icon(Icons.send, size: 24, icon: Icon(Icons.send, size: 28,
color: canSend ? const Color(0xFF8B9CF7) : const Color(0xFFCCCCCC)), color: canSend ? AppTheme.primary : const Color(0xFFCCCCCC)),
onPressed: canSend ? _send : null, onPressed: canSend ? _send : null,
), ),
]), ]),

View File

@@ -351,9 +351,9 @@ class _DietCapturePageState extends ConsumerState<DietCapturePage> {
: null, : null,
), ),
child: Column(mainAxisSize: MainAxisSize.min, children: [ child: Column(mainAxisSize: MainAxisSize.min, children: [
Icon(m.icon, size: 20, color: isSelected ? Colors.white : _kSubText), Icon(m.icon, size: 23, color: isSelected ? Colors.white : _kSubText),
const SizedBox(height: 4), const SizedBox(height: 4),
Text(m.label, style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: isSelected ? Colors.white : _kSubText)), Text(m.label, style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: isSelected ? Colors.white : _kSubText)),
]), ]),
), ),
), ),
@@ -376,15 +376,15 @@ class _DietCapturePageState extends ConsumerState<DietCapturePage> {
alignment: Alignment.center, alignment: Alignment.center,
children: [ children: [
const CircularProgressIndicator(strokeWidth: 3, color: _kPrimary), const CircularProgressIndicator(strokeWidth: 3, color: _kPrimary),
const Icon(Icons.restaurant, size: 22, color: _kPrimary), const Icon(Icons.restaurant, size: 25, color: _kPrimary),
], ],
), ),
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
const Text('正在识别食物...', style: TextStyle(fontSize: 15, color: _kSubText)), const Text('正在识别食物...', style: TextStyle(fontSize: 18, color: _kSubText)),
if (state.errorMessage != null) ...[ if (state.errorMessage != null) ...[
const SizedBox(height: 8), const SizedBox(height: 8),
Text(state.errorMessage!, style: const TextStyle(fontSize: 13, color: AppTheme.error), textAlign: TextAlign.center), Text(state.errorMessage!, style: const TextStyle(fontSize: 16, color: AppTheme.error), textAlign: TextAlign.center),
], ],
]), ]),
), ),
@@ -411,16 +411,16 @@ class _DietCapturePageState extends ConsumerState<DietCapturePage> {
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(color: _kPrimaryLight, borderRadius: BorderRadius.circular(8)), decoration: BoxDecoration(color: _kPrimaryLight, borderRadius: BorderRadius.circular(8)),
child: const Text('识别结果', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: _kPrimary)), child: const Text('识别结果', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: _kPrimary)),
), ),
const Spacer(), const Spacer(),
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(color: const Color(0xFFFFF8EE), borderRadius: BorderRadius.circular(8)), decoration: BoxDecoration(color: const Color(0xFFFFF8EE), borderRadius: BorderRadius.circular(8)),
child: Text('$totalCal kcal', style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: _kWarning)), child: Text('$totalCal kcal', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: _kWarning)),
), ),
IconButton( IconButton(
icon: const Icon(Icons.add_circle_outline, size: 22, color: _kPrimary), icon: const Icon(Icons.add_circle_outline, size: 25, color: _kPrimary),
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
constraints: const BoxConstraints(), constraints: const BoxConstraints(),
onPressed: () => ref.read(dietProvider.notifier).addFood(), onPressed: () => ref.read(dietProvider.notifier).addFood(),
@@ -457,7 +457,7 @@ class _DietCapturePageState extends ConsumerState<DietCapturePage> {
color: food.selected ? _kPrimary : Colors.white, color: food.selected ? _kPrimary : Colors.white,
border: Border.all(color: food.selected ? _kPrimary : const Color(0xFFCCCCCC), width: 2), border: Border.all(color: food.selected ? _kPrimary : const Color(0xFFCCCCCC), width: 2),
), ),
child: food.selected ? const Icon(Icons.check, size: 14, color: Colors.white) : null, child: food.selected ? const Icon(Icons.check, size: 17, color: Colors.white) : null,
), ),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
@@ -466,7 +466,7 @@ class _DietCapturePageState extends ConsumerState<DietCapturePage> {
TextField( TextField(
controller: TextEditingController(text: food.name), controller: TextEditingController(text: food.name),
onChanged: (v) => ref.read(dietProvider.notifier).updateFoodName(food.id, v), onChanged: (v) => ref.read(dietProvider.notifier).updateFoodName(food.id, v),
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: _kText), style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: _kText),
decoration: const InputDecoration( decoration: const InputDecoration(
isDense: true, isDense: true,
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
@@ -480,13 +480,13 @@ class _DietCapturePageState extends ConsumerState<DietCapturePage> {
child: TextField( child: TextField(
controller: TextEditingController(text: food.portion), controller: TextEditingController(text: food.portion),
onChanged: (v) => ref.read(dietProvider.notifier).updateFoodPortion(food.id, v), onChanged: (v) => ref.read(dietProvider.notifier).updateFoodPortion(food.id, v),
style: const TextStyle(fontSize: 12, color: _kSubText), style: const TextStyle(fontSize: 15, color: _kSubText),
decoration: const InputDecoration( decoration: const InputDecoration(
isDense: true, isDense: true,
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
border: InputBorder.none, border: InputBorder.none,
hintText: '份量', hintText: '份量',
hintStyle: TextStyle(fontSize: 12, color: AppTheme.textHint), hintStyle: TextStyle(fontSize: 15, color: AppTheme.textHint),
), ),
), ),
), ),
@@ -497,24 +497,24 @@ class _DietCapturePageState extends ConsumerState<DietCapturePage> {
controller: TextEditingController(text: food.calories.toString()), controller: TextEditingController(text: food.calories.toString()),
onChanged: (v) => ref.read(dietProvider.notifier).updateFoodCalories(food.id, int.tryParse(v) ?? 0), onChanged: (v) => ref.read(dietProvider.notifier).updateFoodCalories(food.id, int.tryParse(v) ?? 0),
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: _kWarning), style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: _kWarning),
textAlign: TextAlign.right, textAlign: TextAlign.right,
decoration: const InputDecoration( decoration: const InputDecoration(
isDense: true, isDense: true,
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
border: InputBorder.none, border: InputBorder.none,
hintText: '0', hintText: '0',
hintStyle: TextStyle(fontSize: 12), hintStyle: TextStyle(fontSize: 15),
), ),
), ),
), ),
const SizedBox(width: 2), const SizedBox(width: 2),
const Text('kcal', style: TextStyle(fontSize: 11, color: _kSubText)), const Text('kcal', style: TextStyle(fontSize: 14, color: _kSubText)),
]), ]),
]), ]),
), ),
IconButton( IconButton(
icon: const Icon(Icons.close, size: 16, color: Color(0xFFBBBBBB)), icon: const Icon(Icons.close, size: 19, color: Color(0xFFBBBBBB)),
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
constraints: const BoxConstraints(), constraints: const BoxConstraints(),
onPressed: () => ref.read(dietProvider.notifier).removeFood(food.id), onPressed: () => ref.read(dietProvider.notifier).removeFood(food.id),
@@ -552,8 +552,8 @@ class _DietCapturePageState extends ConsumerState<DietCapturePage> {
), ),
), ),
Column(mainAxisSize: MainAxisSize.min, children: [ Column(mainAxisSize: MainAxisSize.min, children: [
Text('$totalCalories', style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w800, color: Colors.white)), Text('$totalCalories', style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w800, color: Colors.white)),
Text('kcal', style: const TextStyle(fontSize: 10, color: Colors.white70)), Text('kcal', style: const TextStyle(fontSize: 13, color: Colors.white70)),
]), ]),
], ],
), ),
@@ -561,7 +561,7 @@ class _DietCapturePageState extends ConsumerState<DietCapturePage> {
const SizedBox(width: 16), const SizedBox(width: 16),
Expanded( Expanded(
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
const Text('本餐热量', style: TextStyle(fontSize: 14, color: Colors.white70)), const Text('本餐热量', style: TextStyle(fontSize: 17, color: Colors.white70)),
const SizedBox(height: 4), const SizedBox(height: 4),
Row(children: [ Row(children: [
Expanded(child: _macroBar('碳水', 0.55, const Color(0xFFFFF9C4))), Expanded(child: _macroBar('碳水', 0.55, const Color(0xFFFFF9C4))),
@@ -582,7 +582,7 @@ class _DietCapturePageState extends ConsumerState<DietCapturePage> {
Row(children: [ Row(children: [
Container(width: 6, height: 6, decoration: BoxDecoration(color: color, shape: BoxShape.circle)), Container(width: 6, height: 6, decoration: BoxDecoration(color: color, shape: BoxShape.circle)),
const SizedBox(width: 4), const SizedBox(width: 4),
Text(label, style: const TextStyle(fontSize: 10, color: Colors.white70)), Text(label, style: const TextStyle(fontSize: 13, color: Colors.white70)),
]), ]),
const SizedBox(height: 3), const SizedBox(height: 3),
ClipRRect( ClipRRect(
@@ -607,10 +607,10 @@ class _DietCapturePageState extends ConsumerState<DietCapturePage> {
Container( Container(
width: 32, height: 32, width: 32, height: 32,
decoration: BoxDecoration(color: _kPrimaryLight, borderRadius: BorderRadius.circular(8)), decoration: BoxDecoration(color: _kPrimaryLight, borderRadius: BorderRadius.circular(8)),
child: const Icon(Icons.auto_awesome, size: 16, color: _kPrimary), child: const Icon(Icons.auto_awesome, size: 19, color: _kPrimary),
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
Expanded(child: Text(text, style: const TextStyle(fontSize: 13, color: _kText, height: 1.6))), Expanded(child: Text(text, style: const TextStyle(fontSize: 16, color: _kText, height: 1.6))),
]), ]),
), ),
); );
@@ -650,11 +650,11 @@ class _DietCapturePageState extends ConsumerState<DietCapturePage> {
backgroundColor: _kPrimary, backgroundColor: _kPrimary,
foregroundColor: Colors.white, foregroundColor: Colors.white,
elevation: 0, elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppTheme.radiusMd)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppTheme.rMd)),
textStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600), textStyle: const TextStyle(fontSize: 19, fontWeight: FontWeight.w600),
), ),
child: const Row(mainAxisAlignment: MainAxisAlignment.center, children: [ child: const Row(mainAxisAlignment: MainAxisAlignment.center, children: [
Icon(Icons.check_circle_outline, size: 20), Icon(Icons.check_circle_outline, size: 23),
SizedBox(width: 8), SizedBox(width: 8),
Text('保存记录'), Text('保存记录'),
]), ]),

View File

@@ -1,14 +1,16 @@
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:file_picker/file_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/auth_provider.dart';
import '../../providers/chat_provider.dart'; import '../../providers/chat_provider.dart';
import '../../providers/data_providers.dart'; import '../../providers/data_providers.dart';
import '../diet/diet_capture_page.dart';
import '../../core/navigation_provider.dart';
import '../../widgets/health_drawer.dart'; import '../../widgets/health_drawer.dart';
import '../diet/diet_capture_page.dart';
import 'widgets/chat_messages_view.dart'; import 'widgets/chat_messages_view.dart';
class HomePage extends ConsumerStatefulWidget { class HomePage extends ConsumerStatefulWidget {
@@ -21,9 +23,10 @@ class _HomePageState extends ConsumerState<HomePage> {
final _scrollCtrl = ScrollController(); final _scrollCtrl = ScrollController();
String? _pickedImagePath; String? _pickedImagePath;
final Set<ActiveAgent> _welcomedAgents = {}; final Set<ActiveAgent> _welcomedAgents = {};
final _focusNode = FocusNode();
@override void initState() { super.initState(); } @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() { void _sendMessage() {
final text = _textCtrl.text.trim(); final text = _textCtrl.text.trim();
@@ -43,62 +46,60 @@ class _HomePageState extends ConsumerState<HomePage> {
final auth = ref.watch(authProvider); final auth = ref.watch(authProvider);
final user = auth.user; final user = auth.user;
final selectedAgent = ref.watch(selectedAgentProvider); final selectedAgent = ref.watch(selectedAgentProvider);
final theme = ShadTheme.of(context);
ref.listen(cameraActionProvider, (prev, next) { ref.listen(cameraActionProvider, (prev, next) {
if (next == 'camera') { if (next == 'camera') { _pickImage(ImageSource.camera); ref.read(cameraActionProvider.notifier).clear(); }
_pickImage(ImageSource.camera); else if (next == 'gallery') { _pickImage(ImageSource.gallery); ref.read(cameraActionProvider.notifier).clear(); }
ref.read(cameraActionProvider.notifier).clear();
} else if (next == 'gallery') {
_pickImage(ImageSource.gallery);
ref.read(cameraActionProvider.notifier).clear();
}
}); });
ref.listen(dietActionProvider, (prev, next) { ref.listen(dietActionProvider, (prev, next) {
if (next == 'pickFoodCamera') { if (next == 'pickFoodCamera') { _pickFoodImage(ImageSource.camera); ref.read(dietActionProvider.notifier).clear(); }
_pickFoodImage(ImageSource.camera); else if (next == 'pickFoodGallery') { _pickFoodImage(ImageSource.gallery); ref.read(dietActionProvider.notifier).clear(); }
ref.read(dietActionProvider.notifier).clear();
} else if (next == 'pickFoodGallery') {
_pickFoodImage(ImageSource.gallery);
ref.read(dietActionProvider.notifier).clear();
}
}); });
return Scaffold( return Scaffold(
drawer: const HealthDrawer(), drawer: const HealthDrawer(),
backgroundColor: const Color(0xFFF8F9FC), backgroundColor: theme.colorScheme.background,
body: SafeArea( body: SafeArea(
child: Column(children: [ child: Column(children: [
// ── 顶部栏 ── _buildHeader(user, theme),
_buildHeader(user),
// ── 聊天区域(今日任务在对话流最上方)──
Expanded(child: ChatMessagesView(scrollCtrl: _scrollCtrl, messages: chatState.messages)), Expanded(child: ChatMessagesView(scrollCtrl: _scrollCtrl, messages: chatState.messages)),
_buildBottomBar(context, selectedAgent, theme),
// ── 底部合并区 ──
_buildBottomBar(context, selectedAgent),
]), ]),
), ),
); );
} }
// ═════════════════════ 顶部栏 ═════════════════════ // ═══════ 顶部栏 ═══════
Widget _buildHeader(dynamic user, ShadThemeData theme) {
Widget _buildHeader(dynamic user) {
return Container( 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: [ child: Row(children: [
Builder(builder: (ctx) => GestureDetector( Builder(builder: (ctx) => GestureDetector(
onTap: () => Scaffold.of(ctx).openDrawer(), 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), const SizedBox(width: 10),
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ 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), 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 '晚上好'; return '晚上好';
} }
// ═════════════════════ 智能体选择条(常驻) ═════════════════════ // ═══════ 智能体选择条 ═══════
static final _agentDefs = [ static final _agentDefs = [
(ActiveAgent.consultation, '问诊', Icons.chat_bubble_outline), (ActiveAgent.consultation, '问诊', LucideIcons.messageCircle),
(ActiveAgent.health, '记数据', Icons.favorite_border), (ActiveAgent.health, '记数据', LucideIcons.heart),
(ActiveAgent.diet, '拍饮食', Icons.restaurant_outlined), (ActiveAgent.diet, '拍饮食', LucideIcons.utensils),
(ActiveAgent.medication, '药管家', Icons.medication_outlined), (ActiveAgent.medication, '药管家', LucideIcons.pill),
(ActiveAgent.report, '看报告', Icons.description_outlined), (ActiveAgent.report, '看报告', LucideIcons.fileText),
(ActiveAgent.exercise, '运动', Icons.directions_run_outlined), (ActiveAgent.exercise, '运动', LucideIcons.trendingUp),
]; ];
Widget _buildAgentBar(ActiveAgent? selected) { Widget _buildAgentBar(ActiveAgent? selected, ShadThemeData theme) {
return Container( return Container(
height: 36, height: 40,
padding: const EdgeInsets.symmetric(horizontal: 12), padding: const EdgeInsets.symmetric(horizontal: AppTheme.sMd),
child: ListView.separated( child: ListView.separated(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
itemCount: _agentDefs.length, itemCount: _agentDefs.length,
@@ -146,17 +146,18 @@ class _HomePageState extends ConsumerState<HomePage> {
} }
} }
}, },
child: Container( child: AnimatedContainer(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isActive ? const Color(0xFF8B9CF7) : Colors.white, color: isActive ? AppTheme.primary : theme.colorScheme.card,
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(AppTheme.rPill),
border: Border.all(color: isActive ? const Color(0xFF8B9CF7) : const Color(0xFFE0E0E0)), boxShadow: isActive ? [AppTheme.shadowLight] : null,
), ),
child: Row(mainAxisSize: MainAxisSize.min, children: [ child: Row(mainAxisSize: MainAxisSize.min, children: [
Icon(icon, size: 13, color: isActive ? Colors.white : const Color(0xFF666666)), Icon(icon, size: 17, color: isActive ? Colors.white : theme.colorScheme.mutedForeground),
const SizedBox(width: 3), const SizedBox(width: 4),
Text(label, style: TextStyle(fontSize: 11, fontWeight: isActive ? FontWeight.w600 : FontWeight.w500, color: isActive ? Colors.white : const Color(0xFF666666))), 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, ShadThemeData theme) {
Widget _buildBottomBar(BuildContext context, ActiveAgent? selectedAgent) {
return Column(mainAxisSize: MainAxisSize.min, children: [ return Column(mainAxisSize: MainAxisSize.min, children: [
// 智能体胶囊栏常驻高度36 Container(
_buildAgentBar(selectedAgent), padding: const EdgeInsets.only(top: AppTheme.sSm, bottom: 6),
child: _buildAgentBar(selectedAgent, theme),
// 图片预览(有选中图片时显示) ),
if (_pickedImagePath != null) _buildImagePreview(), if (_pickedImagePath != null) _buildImagePreview(theme),
_buildCompactInputBar(theme),
// 输入框
_buildCompactInputBar(context),
]); ]);
} }
Widget _buildImagePreview() { Widget _buildImagePreview(ShadThemeData theme) {
return Container( return Container(
padding: const EdgeInsets.fromLTRB(12, 8, 12, 4), 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: [ child: Row(children: [
Stack(children: [ Stack(children: [
ClipRRect( ClipRRect(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(AppTheme.rXs),
child: Image.file(File(_pickedImagePath!), width: 60, height: 60, fit: BoxFit.cover), child: Image.file(File(_pickedImagePath!), width: 60, height: 60, fit: BoxFit.cover),
), ),
Positioned(top: -4, right: -4, child: GestureDetector( Positioned(top: -4, right: -4, child: GestureDetector(
onTap: () => setState(() => _pickedImagePath = null), 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(), 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( return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: AppTheme.sSm, vertical: AppTheme.sSm),
decoration: BoxDecoration(color: Colors.white, border: Border(top: BorderSide(color: const Color(0xFFEEEEEE)))), decoration: BoxDecoration(
child: Row(children: [ color: theme.colorScheme.card,
IconButton(icon: const Icon(Icons.attach_file, size: 24, color: Color(0xFF666666)), onPressed: () => _showAttachmentPicker(context)), border: Border(top: BorderSide(color: theme.colorScheme.border)),
Expanded(child: TextField( ),
controller: _textCtrl, child: Row(crossAxisAlignment: CrossAxisAlignment.end, children: [
style: const TextStyle(fontSize: 15), ShadButton.ghost(
decoration: const InputDecoration(hintText: '输入你想说的...', hintStyle: TextStyle(fontSize: 15, color: Color(0xFFBBBBBB)), contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 10), border: InputBorder.none), size: ShadButtonSize.sm,
onSubmitted: (_) => _sendMessage(), leading: Icon(LucideIcons.paperclip, size: 25, color: theme.colorScheme.mutedForeground),
)), onPressed: () => _showAttachmentPicker(context, theme),
IconButton(icon: const Icon(Icons.send, size: 24, color: Color(0xFF8B9CF7)), onPressed: _sendMessage), ),
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 { Future<void> _pickImage(ImageSource source) async {
final picker = ImagePicker(); final picker = ImagePicker();
final picked = await picker.pickImage(source: source, imageQuality: 85); final picked = await picker.pickImage(source: source, imageQuality: 85);
@@ -238,13 +317,4 @@ class _HomePageState extends ConsumerState<HomePage> {
pushRoute(ref, 'dietCapture'); 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(() {}); }}),
])));
}
} }

View File

@@ -2,6 +2,7 @@ import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/app_theme.dart';
import '../../../core/navigation_provider.dart'; import '../../../core/navigation_provider.dart';
import '../../../providers/auth_provider.dart'; import '../../../providers/auth_provider.dart';
import '../../../providers/chat_provider.dart'; import '../../../providers/chat_provider.dart';
@@ -27,15 +28,15 @@ class ChatMessagesView extends ConsumerWidget {
width: 80, width: 80,
height: 80, height: 80,
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFF0F2FF), color: AppTheme.primaryLight,
borderRadius: BorderRadius.circular(40), borderRadius: BorderRadius.circular(40),
), ),
child: const Icon(Icons.health_and_safety, size: 40, color: Color(0xFF8B9CF7)), child: const Icon(Icons.health_and_safety, size: 40, color: AppTheme.primary),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Text('开始和 AI 健康管家对话吧', style: Theme.of(context).textTheme.bodyMedium), Text('开始和 AI 健康管家对话吧', style: Theme.of(context).textTheme.bodyMedium),
const SizedBox(height: 8), const SizedBox(height: 8),
const Text('记录健康数据,获取专业建议', style: TextStyle(fontSize: 14, color: Color(0xFF9E9E9E))), const Text('记录健康数据,获取专业建议', style: TextStyle(fontSize: 17, color: Color(0xFF9E9E9E))),
], ],
), ),
); );
@@ -135,7 +136,7 @@ class ChatMessagesView extends ConsumerWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(info.$2, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w700, color: Colors.white)), Text(info.$2, style: const TextStyle(fontSize: 21, fontWeight: FontWeight.w700, color: Colors.white)),
const SizedBox(height: 4), const SizedBox(height: 4),
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 3), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 3),
@@ -143,7 +144,7 @@ class ChatMessagesView extends ConsumerWidget {
color: Colors.white.withAlpha(25), color: Colors.white.withAlpha(25),
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
), ),
child: Text(info.$3, style: const TextStyle(fontSize: 12, color: Color(0xFFE8E6FF))), child: Text(info.$3, style: const TextStyle(fontSize: 15, color: Color(0xFFE8E6FF))),
), ),
], ],
), ),
@@ -210,10 +211,10 @@ class ChatMessagesView extends ConsumerWidget {
color: colors.iconBg, color: colors.iconBg,
borderRadius: BorderRadius.circular(11), borderRadius: BorderRadius.circular(11),
), ),
child: Icon(a.icon, size: 20, color: colors.accent), child: Icon(a.icon, size: 23, color: colors.accent),
), ),
const SizedBox(height: 7), const SizedBox(height: 7),
Text(a.label, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500, color: Color(0xFF333333))), Text(a.label, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: Color(0xFF333333))),
], ],
), ),
), ),
@@ -245,30 +246,30 @@ class ChatMessagesView extends ConsumerWidget {
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFFF0F2FF)), border: Border.all(color: AppTheme.primaryLight),
), ),
child: Column(mainAxisSize: MainAxisSize.min, children: [ child: Column(mainAxisSize: MainAxisSize.min, children: [
CircleAvatar( CircleAvatar(
radius: 22, radius: 22,
backgroundColor: const Color(0xFFF0F2FF), backgroundColor: AppTheme.primaryLight,
child: Text(doc['name']![0], style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Color(0xFF8B9CF7))), child: Text(doc['name']![0], style: const TextStyle(fontSize: 21, fontWeight: FontWeight.w600, color: AppTheme.primary)),
), ),
const SizedBox(height: 6), const SizedBox(height: 6),
Text(doc['name']!, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: Color(0xFF333333))), Text(doc['name']!, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF333333))),
Text(doc['title']!, style: const TextStyle(fontSize: 11, color: Color(0xFF999999))), Text(doc['title']!, style: const TextStyle(fontSize: 14, color: Color(0xFF999999))),
const SizedBox(height: 2), const SizedBox(height: 2),
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFF0F2FF), color: AppTheme.primaryLight,
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
), ),
child: Text(doc['dept']!, style: const TextStyle(fontSize: 10, color: Color(0xFF8B9CF7))), child: Text(doc['dept']!, style: const TextStyle(fontSize: 13, color: AppTheme.primary)),
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
doc['desc']!, doc['desc']!,
style: const TextStyle(fontSize: 10, color: Color(0xFF888888), height: 1.3), style: const TextStyle(fontSize: 13, color: Color(0xFF888888), height: 1.3),
maxLines: 2, maxLines: 2,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center, textAlign: TextAlign.center,
@@ -298,34 +299,34 @@ class ChatMessagesView extends ConsumerWidget {
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFFFFFFF), color: const Color(0xFFFFFFFF),
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
boxShadow: [BoxShadow(color: const Color(0xFF6C5CE7).withAlpha(15), blurRadius: 14, offset: const Offset(0, 4))], boxShadow: [BoxShadow(color: AppTheme.primary.withAlpha(15), blurRadius: 14, offset: const Offset(0, 4))],
), ),
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: Column(mainAxisSize: MainAxisSize.min, children: [ child: Column(mainAxisSize: MainAxisSize.min, children: [
Container( Container(
width: double.infinity, padding: const EdgeInsets.symmetric(vertical: 10), width: double.infinity, padding: const EdgeInsets.symmetric(vertical: 10),
decoration: const BoxDecoration(gradient: LinearGradient(colors: [Color(0xFF4CAF50), Color(0xFF43A047)])), decoration: const BoxDecoration(gradient: LinearGradient(colors: [Color(0xFF4CAF50), AppTheme.success])),
child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [ child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [
const Icon(Icons.info_outline, size: 18, color: Colors.white), const SizedBox(width: 6), const Icon(Icons.info_outline, size: 21, color: Colors.white), const SizedBox(width: 6),
Text(metricType == 'exercise' ? '请确认运动计划' : '请确认录入', Text(metricType == 'exercise' ? '请确认运动计划' : '请确认录入',
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Colors.white)), style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w600, color: Colors.white)),
]), ]),
), ),
Padding(padding: const EdgeInsets.all(18), child: Column(children: [ Padding(padding: const EdgeInsets.all(18), child: Column(children: [
Align(alignment: Alignment.centerLeft, child: Text(recordTime.isNotEmpty ? recordTime : _formatTime(msg.createdAt), style: const TextStyle(fontSize: 12, color: Color(0xFF9E9E9E)))), Align(alignment: Alignment.centerLeft, child: Text(recordTime.isNotEmpty ? recordTime : _formatTime(msg.createdAt), style: const TextStyle(fontSize: 15, color: Color(0xFF9E9E9E)))),
const SizedBox(height: 14), const SizedBox(height: 14),
Container( Container(
padding: const EdgeInsets.all(18), padding: const EdgeInsets.all(18),
decoration: BoxDecoration(color: const Color(0xFFF9F8FF), borderRadius: BorderRadius.circular(16)), decoration: BoxDecoration(color: const Color(0xFFF9F8FF), borderRadius: BorderRadius.circular(16)),
child: Row(children: [ child: Row(children: [
Container(width: 52, height: 52, decoration: BoxDecoration(color: const Color(0xFFEDEAFF), borderRadius: BorderRadius.circular(14)), child: Center(child: Text(_getMetricIcon(metricType), style: const TextStyle(fontSize: 26)))), Container(width: 52, height: 52, decoration: BoxDecoration(color: AppTheme.primaryLight, borderRadius: BorderRadius.circular(14)), child: Center(child: Text(_getMetricIcon(metricType), style: const TextStyle(fontSize: 30)))),
const SizedBox(width: 14), const SizedBox(width: 14),
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(_getMetricName(metricType), style: const TextStyle(fontSize: 13, color: Color(0xFF888888))), Text(_getMetricName(metricType), style: const TextStyle(fontSize: 16, color: Color(0xFF888888))),
const SizedBox(height: 4), const SizedBox(height: 4),
RichText(text: TextSpan(children: [ RichText(text: TextSpan(children: [
TextSpan(text: value, style: TextStyle(fontSize: 28, fontWeight: FontWeight.w800, color: abnormal ? const Color(0xFFE53935) : const Color(0xFF1A1A2E))), TextSpan(text: value, style: TextStyle(fontSize: 32, fontWeight: FontWeight.w800, color: abnormal ? AppTheme.error : const Color(0xFF1A1A2E))),
TextSpan(text: ' $unit', style: TextStyle(fontSize: 14, color: abnormal ? const Color(0xFFE53970) : const Color(0xFF999999))), TextSpan(text: ' $unit', style: TextStyle(fontSize: 17, color: abnormal ? const Color(0xFFE53970) : const Color(0xFF999999))),
])), ])),
])), ])),
]), ]),
@@ -333,7 +334,7 @@ class ChatMessagesView extends ConsumerWidget {
if (abnormal) ...[ if (abnormal) ...[
const SizedBox(height: 12), const SizedBox(height: 12),
Container(width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), decoration: BoxDecoration(color: const Color(0xFFFFF3F0), borderRadius: BorderRadius.circular(10), border: Border.all(color: const Color(0xFFFFDAD4))), Container(width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), decoration: BoxDecoration(color: const Color(0xFFFFF3F0), borderRadius: BorderRadius.circular(10), border: Border.all(color: const Color(0xFFFFDAD4))),
child: const Row(children: [Icon(Icons.warning_amber_rounded, size: 18, color: Color(0xFFE53935)), SizedBox(width: 8), Expanded(child: Text('数值偏高,建议关注', style: TextStyle(fontSize: 13, color: Color(0xFFE53935), fontWeight: FontWeight.w500)))])), child: const Row(children: [Icon(Icons.warning_amber_rounded, size: 21, color: AppTheme.error), SizedBox(width: 8), Expanded(child: Text('数值偏高,建议关注', style: TextStyle(fontSize: 16, color: AppTheme.error, fontWeight: FontWeight.w500)))])),
], ],
const SizedBox(height: 18), const SizedBox(height: 18),
if (msg.confirmed) if (msg.confirmed)
@@ -341,12 +342,12 @@ class ChatMessagesView extends ConsumerWidget {
width: double.infinity, width: double.infinity,
child: ElevatedButton.icon( child: ElevatedButton.icon(
onPressed: null, onPressed: null,
icon: const Icon(Icons.check_circle, size: 18), icon: const Icon(Icons.check_circle, size: 21),
label: const Text('录入成功'), label: const Text('录入成功'),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF43A047), backgroundColor: AppTheme.success,
foregroundColor: Colors.white, foregroundColor: Colors.white,
disabledBackgroundColor: const Color(0xFF43A047), disabledBackgroundColor: AppTheme.success,
disabledForegroundColor: Colors.white, disabledForegroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
padding: const EdgeInsets.symmetric(vertical: 12), padding: const EdgeInsets.symmetric(vertical: 12),
@@ -388,7 +389,7 @@ class ChatMessagesView extends ConsumerWidget {
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFFFFFFF), color: const Color(0xFFFFFFFF),
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
boxShadow: [BoxShadow(color: const Color(0xFF8B9CF7).withAlpha(15), blurRadius: 14, offset: const Offset(0, 4))], boxShadow: [BoxShadow(color: AppTheme.primary.withAlpha(15), blurRadius: 14, offset: const Offset(0, 4))],
), ),
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: Column( child: Column(
@@ -399,7 +400,7 @@ class ChatMessagesView extends ConsumerWidget {
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.fromLTRB(18, 20, 18, 16), padding: const EdgeInsets.fromLTRB(18, 20, 18, 16),
decoration: const BoxDecoration( decoration: const BoxDecoration(
gradient: LinearGradient(colors: [Color(0xFFE8F0FE), Color(0xFFF0F2FF)]), gradient: LinearGradient(colors: [Color(0xFFE8F0FE), AppTheme.primaryLight]),
), ),
child: Row( child: Row(
children: [ children: [
@@ -410,17 +411,17 @@ class ChatMessagesView extends ConsumerWidget {
color: const Color(0xFFFFF3E0), color: const Color(0xFFFFF3E0),
borderRadius: BorderRadius.circular(13), borderRadius: BorderRadius.circular(13),
), ),
child: const Center(child: Text('💊', style: TextStyle(fontSize: 24))), child: const Center(child: Text('💊', style: TextStyle(fontSize: 28))),
), ),
const SizedBox(width: 14), const SizedBox(width: 14),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(name, style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w700, color: Color(0xFF1A1A2E))), Text(name, style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w700, color: Color(0xFF1A1A2E))),
if (dosage.isNotEmpty) ...[ if (dosage.isNotEmpty) ...[
const SizedBox(height: 3), const SizedBox(height: 3),
Text(dosage, style: const TextStyle(fontSize: 13, color: Color(0xFF777777))), Text(dosage, style: const TextStyle(fontSize: 16, color: Color(0xFF777777))),
], ],
], ],
), ),
@@ -444,12 +445,12 @@ class ChatMessagesView extends ConsumerWidget {
width: double.infinity, width: double.infinity,
child: ElevatedButton.icon( child: ElevatedButton.icon(
onPressed: null, onPressed: null,
icon: const Icon(Icons.check_circle, size: 18), icon: const Icon(Icons.check_circle, size: 21),
label: const Text('录入成功'), label: const Text('录入成功'),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF43A047), backgroundColor: AppTheme.success,
foregroundColor: Colors.white, foregroundColor: Colors.white,
disabledBackgroundColor: const Color(0xFF43A047), disabledBackgroundColor: AppTheme.success,
disabledForegroundColor: Colors.white, disabledForegroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
padding: const EdgeInsets.symmetric(vertical: 12), padding: const EdgeInsets.symmetric(vertical: 12),
@@ -479,9 +480,9 @@ class ChatMessagesView extends ConsumerWidget {
padding: const EdgeInsets.only(bottom: 10), padding: const EdgeInsets.only(bottom: 10),
child: Row( child: Row(
children: [ children: [
Icon(icon, size: 17, color: const Color(0xFF888888)), Icon(icon, size: 20, color: const Color(0xFF888888)),
const SizedBox(width: 8), const SizedBox(width: 8),
Text(text, style: const TextStyle(fontSize: 13, color: Color(0xFF444444))), Text(text, style: const TextStyle(fontSize: 16, color: Color(0xFF444444))),
], ],
), ),
); );
@@ -514,7 +515,7 @@ class ChatMessagesView extends ConsumerWidget {
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFFFFFFF), color: const Color(0xFFFFFFFF),
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
boxShadow: [BoxShadow(color: const Color(0xFF8B9CF7).withAlpha(15), blurRadius: 14, offset: const Offset(0, 4))], boxShadow: [BoxShadow(color: AppTheme.primary.withAlpha(15), blurRadius: 14, offset: const Offset(0, 4))],
), ),
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: Column( child: Column(
@@ -526,8 +527,8 @@ class ChatMessagesView extends ConsumerWidget {
padding: const EdgeInsets.symmetric(vertical: 12), padding: const EdgeInsets.symmetric(vertical: 12),
decoration: const BoxDecoration(gradient: LinearGradient(colors: [Color(0xFFFFF8E1), Color(0xFFFFF3E0)])), decoration: const BoxDecoration(gradient: LinearGradient(colors: [Color(0xFFFFF8E1), Color(0xFFFFF3E0)])),
child: const Row(mainAxisAlignment: MainAxisAlignment.center, children: [ child: const Row(mainAxisAlignment: MainAxisAlignment.center, children: [
Text('🍽️ ', style: TextStyle(fontSize: 18)), Text('🍽️ ', style: TextStyle(fontSize: 21)),
Text('饮食分析结果', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: Color(0xFF1A1A2E))), Text('饮食分析结果', style: TextStyle(fontSize: 19, fontWeight: FontWeight.w700, color: Color(0xFF1A1A2E))),
]), ]),
), ),
Padding( Padding(
@@ -536,15 +537,15 @@ class ChatMessagesView extends ConsumerWidget {
// ── 总热量(仅 >0 时显示) ── // ── 总热量(仅 >0 时显示) ──
if (totalCalories > 0) ...[ if (totalCalories > 0) ...[
Center(child: Column(children: [ Center(child: Column(children: [
Text('$totalCalories', style: const TextStyle(fontSize: 36, fontWeight: FontWeight.w800, color: Color(0xFFFF8F00))), Text('$totalCalories', style: const TextStyle(fontSize: 40, fontWeight: FontWeight.w800, color: Color(0xFFFF8F00))),
const Text('千卡 (kcal)', style: TextStyle(fontSize: 12, color: Color(0xFFAAAAAA))), const Text('千卡 (kcal)', style: TextStyle(fontSize: 15, color: Color(0xFFAAAAAA))),
])), ])),
const SizedBox(height: 16), const SizedBox(height: 16),
], ],
// ── 识别食物列表 ── // ── 识别食物列表 ──
if (foods.isNotEmpty) ...[ if (foods.isNotEmpty) ...[
const Text('识别结果', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))), const Text('识别结果', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
const SizedBox(height: 10), const SizedBox(height: 10),
...foods.map((food) { ...foods.map((food) {
final f = food is Map ? food : <String, dynamic>{}; final f = food is Map ? food : <String, dynamic>{};
@@ -559,11 +560,11 @@ class ChatMessagesView extends ConsumerWidget {
decoration: BoxDecoration(color: const Color(0xFFFAFAFA), borderRadius: BorderRadius.circular(10), border: Border.all(color: const Color(0xFFF0F0F0))), decoration: BoxDecoration(color: const Color(0xFFFAFAFA), borderRadius: BorderRadius.circular(10), border: Border.all(color: const Color(0xFFF0F0F0))),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Row(children: [ Row(children: [
Expanded(child: Text(name, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A)))), Expanded(child: Text(name, style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A)))),
if (calories > 0) Text('${calories is int ? calories : calories.toInt()} kcal', style: const TextStyle(fontSize: 13, color: Color(0xFF888888))), if (calories > 0) Text('${calories is int ? calories : calories.toInt()} kcal', style: const TextStyle(fontSize: 16, color: Color(0xFF888888))),
]), ]),
if (portion != null && portion.isNotEmpty) Padding(padding: const EdgeInsets.only(top: 4), child: Text(portion, style: TextStyle(fontSize: 12, color: Colors.grey[500]))), if (portion != null && portion.isNotEmpty) Padding(padding: const EdgeInsets.only(top: 4), child: Text(portion, style: TextStyle(fontSize: 15, color: Colors.grey[500]))),
if (nutrients != null && nutrients.isNotEmpty) Padding(padding: const EdgeInsets.only(top: 2), child: Text(nutrients, style: TextStyle(fontSize: 11, color: Colors.grey[500]))), if (nutrients != null && nutrients.isNotEmpty) Padding(padding: const EdgeInsets.only(top: 2), child: Text(nutrients, style: TextStyle(fontSize: 14, color: Colors.grey[500]))),
]), ]),
); );
}), }),
@@ -572,12 +573,12 @@ class ChatMessagesView extends ConsumerWidget {
// ── AI 建议 ── // ── AI 建议 ──
const SizedBox(height: 14), const SizedBox(height: 14),
const Text('AI 建议', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))), const Text('AI 建议', style: TextStyle(fontSize: 17, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
const SizedBox(height: 6), const SizedBox(height: 6),
Container( Container(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
decoration: BoxDecoration(color: const Color(0xFFF0F2FF), borderRadius: BorderRadius.circular(10)), decoration: BoxDecoration(color: AppTheme.primaryLight, borderRadius: BorderRadius.circular(10)),
child: Text(advice, style: const TextStyle(fontSize: 13, height: 1.6, color: Color(0xFF555555))), child: Text(advice, style: const TextStyle(fontSize: 16, height: 1.6, color: Color(0xFF555555))),
), ),
]), ]),
), ),
@@ -606,7 +607,7 @@ class ChatMessagesView extends ConsumerWidget {
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFFFFFFF), color: const Color(0xFFFFFFFF),
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
boxShadow: [BoxShadow(color: const Color(0xFF8B9CF7).withAlpha(15), blurRadius: 14, offset: const Offset(0, 4))], boxShadow: [BoxShadow(color: AppTheme.primary.withAlpha(15), blurRadius: 14, offset: const Offset(0, 4))],
), ),
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: Column( child: Column(
@@ -628,16 +629,16 @@ class ChatMessagesView extends ConsumerWidget {
color: const Color(0xFFC5CAE9), color: const Color(0xFFC5CAE9),
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
), ),
child: const Icon(Icons.description_outlined, size: 20, color: Color(0xFF3F51B5)), child: const Icon(Icons.description_outlined, size: 23, color: Color(0xFF3F51B5)),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(reportType, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: Color(0xFF1A1A2E))), Text(reportType, style: const TextStyle(fontSize: 19, fontWeight: FontWeight.w700, color: Color(0xFF1A1A2E))),
if (reportDate.isNotEmpty) if (reportDate.isNotEmpty)
Text(reportDate, style: const TextStyle(fontSize: 12, color: Color(0xFF888888))), Text(reportDate, style: const TextStyle(fontSize: 15, color: Color(0xFF888888))),
], ],
), ),
), ),
@@ -668,9 +669,9 @@ class ChatMessagesView extends ConsumerWidget {
), ),
child: const Row( child: const Row(
children: [ children: [
Expanded(flex: 2, child: Text('指标名称', style: TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: Color(0xFF666666)))), Expanded(flex: 2, child: Text('指标名称', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFF666666)))),
Expanded(flex: 1, child: Text('数值', style: TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: Color(0xFF666666)), textAlign: TextAlign.center)), Expanded(flex: 1, child: Text('数值', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFF666666)), textAlign: TextAlign.center)),
Expanded(flex: 1, child: Text('状态', style: TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: Color(0xFF666666)), textAlign: TextAlign.center)), Expanded(flex: 1, child: Text('状态', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFF666666)), textAlign: TextAlign.center)),
], ],
), ),
), ),
@@ -684,9 +685,9 @@ class ChatMessagesView extends ConsumerWidget {
final isAbnormal = status != 'normal'; final isAbnormal = status != 'normal';
Color sc; Color sc;
switch (status) { switch (status) {
case 'high': sc = const Color(0xFFE53935); break; case 'high': sc = AppTheme.error; break;
case 'low': sc = const Color(0xFFF9A825); break; case 'low': sc = const Color(0xFFF9A825); break;
default: sc = const Color(0xFF43A047); default: sc = AppTheme.success;
} }
return Container( return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
@@ -703,12 +704,12 @@ class ChatMessagesView extends ConsumerWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(name, style: TextStyle(fontSize: 13, color: isAbnormal ? const Color(0xFFE53935) : const Color(0xFF333333), fontWeight: isAbnormal ? FontWeight.w600 : FontWeight.normal)), Text(name, style: TextStyle(fontSize: 16, color: isAbnormal ? AppTheme.error : const Color(0xFF333333), fontWeight: isAbnormal ? FontWeight.w600 : FontWeight.normal)),
if (refRange.isNotEmpty) Text('参考:$refRange', style: const TextStyle(fontSize: 10, color: Color(0xFFAAAAAA))), if (refRange.isNotEmpty) Text('参考:$refRange', style: const TextStyle(fontSize: 13, color: Color(0xFFAAAAAA))),
], ],
), ),
), ),
Expanded(flex: 1, child: Text(value, textAlign: TextAlign.center, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: sc))), Expanded(flex: 1, child: Text(value, textAlign: TextAlign.center, style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: sc))),
Expanded( Expanded(
flex: 1, flex: 1,
child: Center( child: Center(
@@ -743,13 +744,13 @@ class ChatMessagesView extends ConsumerWidget {
children: [ children: [
const Row( const Row(
children: [ children: [
Icon(Icons.auto_awesome, size: 16, color: Color(0xFF8B9CF7)), Icon(Icons.auto_awesome, size: 19, color: AppTheme.primary),
SizedBox(width: 6), SizedBox(width: 6),
Text('AI 解读摘要', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: Color(0xFF8B9CF7))), Text('AI 解读摘要', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: AppTheme.primary)),
], ],
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Text(summary, style: const TextStyle(fontSize: 13, color: Color(0xFF555555), height: 1.5)), Text(summary, style: const TextStyle(fontSize: 16, color: Color(0xFF555555), height: 1.5)),
], ],
), ),
), ),
@@ -786,27 +787,27 @@ class ChatMessagesView extends ConsumerWidget {
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFFFFFFF), color: const Color(0xFFFFFFFF),
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
boxShadow: [BoxShadow(color: const Color(0xFF8B9CF7).withAlpha(15), blurRadius: 14, offset: const Offset(0, 4))], boxShadow: [BoxShadow(color: AppTheme.primary.withAlpha(15), blurRadius: 14, offset: const Offset(0, 4))],
), ),
child: Padding( child: Padding(
padding: const EdgeInsets.fromLTRB(18, 16, 18, 14), padding: const EdgeInsets.fromLTRB(18, 16, 18, 14),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(msg.content, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: Color(0xFF1A1A2E))), Text(msg.content, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w500, color: Color(0xFF1A1A2E))),
const SizedBox(height: 14), const SizedBox(height: 14),
Wrap(spacing: 8, runSpacing: 8, children: options.map((opt) { Wrap(spacing: 8, runSpacing: 8, children: options.map((opt) {
final o = opt as Map? ?? {}; final o = opt as Map? ?? {};
return ElevatedButton( return ElevatedButton(
onPressed: () {}, onPressed: () {},
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFF0F2FF), backgroundColor: AppTheme.primaryLight,
foregroundColor: const Color(0xFF8B9CF7), foregroundColor: AppTheme.primary,
elevation: 0, elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 11), padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 11),
), ),
child: Text(o['label'] as String? ?? '', style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500)), child: Text(o['label'] as String? ?? '', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
); );
}).toList()), }).toList()),
], ],
@@ -830,7 +831,7 @@ class ChatMessagesView extends ConsumerWidget {
color: const Color(0xFFFEFEFF), color: const Color(0xFFFEFEFF),
borderRadius: const BorderRadius.only(topLeft: Radius.circular(4), topRight: Radius.circular(20), bottomLeft: Radius.circular(20), bottomRight: Radius.circular(20)), borderRadius: const BorderRadius.only(topLeft: Radius.circular(4), topRight: Radius.circular(20), bottomLeft: Radius.circular(20), bottomRight: Radius.circular(20)),
border: Border.all(color: const Color(0xFFD8DCFD), width: 1.5), border: Border.all(color: const Color(0xFFD8DCFD), width: 1.5),
boxShadow: [BoxShadow(color: const Color(0xFF8B9CF7).withAlpha(12), blurRadius: 10, offset: const Offset(0, 3))], boxShadow: [BoxShadow(color: AppTheme.primary.withAlpha(12), blurRadius: 10, offset: const Offset(0, 3))],
), ),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -840,13 +841,13 @@ class ChatMessagesView extends ConsumerWidget {
height: 26, height: 26,
padding: const EdgeInsets.all(5), padding: const EdgeInsets.all(5),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFF0F2FF), color: AppTheme.primaryLight,
borderRadius: BorderRadius.circular(13), borderRadius: BorderRadius.circular(13),
), ),
child: const CircularProgressIndicator(strokeWidth: 2.2, color: Color(0xFF8B9CF7)), child: const CircularProgressIndicator(strokeWidth: 2.2, color: AppTheme.primary),
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
Text(thinkingText?.isNotEmpty == true ? thinkingText! : '正在分析...', style: const TextStyle(fontSize: 14, color: Color(0xFF999999))), Text(thinkingText?.isNotEmpty == true ? thinkingText! : '正在分析...', style: const TextStyle(fontSize: 17, color: Color(0xFF999999))),
], ],
), ),
), ),
@@ -866,7 +867,7 @@ class ChatMessagesView extends ConsumerWidget {
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.82), constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.82),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isUser ? const Color(0xFF8B9CF7) : const Color(0xFFFEFEFF), color: isUser ? AppTheme.primary : const Color(0xFFFEFEFF),
borderRadius: BorderRadius.only( borderRadius: BorderRadius.only(
topLeft: Radius.circular(isUser ? 20 : 4), topLeft: Radius.circular(isUser ? 20 : 4),
topRight: Radius.circular(isUser ? 4 : 20), topRight: Radius.circular(isUser ? 4 : 20),
@@ -874,23 +875,23 @@ class ChatMessagesView extends ConsumerWidget {
bottomRight: const Radius.circular(20), bottomRight: const Radius.circular(20),
), ),
border: isUser ? null : Border.all(color: const Color(0xFFD8DCFD), width: 1.5), border: isUser ? null : Border.all(color: const Color(0xFFD8DCFD), width: 1.5),
boxShadow: isUser ? [] : [BoxShadow(color: const Color(0xFF8B9CF7).withAlpha(12), blurRadius: 10, offset: const Offset(0, 3))], boxShadow: isUser ? [] : [BoxShadow(color: AppTheme.primary.withAlpha(12), blurRadius: 10, offset: const Offset(0, 3))],
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// 文字内容 // 文字内容
if (isUser) if (isUser)
SelectableText(msg.content, style: const TextStyle(fontSize: 16, color: Colors.white, height: 1.4)) SelectableText(msg.content, style: const TextStyle(fontSize: 19, color: Colors.white, height: 1.4))
else else
MarkdownBody( MarkdownBody(
data: msg.content, data: msg.content,
selectable: true, selectable: true,
styleSheet: MarkdownStyleSheet( styleSheet: MarkdownStyleSheet(
p: const TextStyle(fontSize: 16, color: Color(0xFF1A1A1A), height: 1.5), p: const TextStyle(fontSize: 19, color: Color(0xFF1A1A1A), height: 1.5),
h1: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A)), h1: const TextStyle(fontSize: 21, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A)),
h2: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A)), h2: const TextStyle(fontSize: 19, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A)),
code: const TextStyle(fontSize: 14, backgroundColor: Color(0xFFF0F2FF)), code: const TextStyle(fontSize: 17, backgroundColor: AppTheme.primaryLight),
), ),
), ),
@@ -910,7 +911,7 @@ class ChatMessagesView extends ConsumerWidget {
? Image.network(imageUrl, fit: BoxFit.cover, errorBuilder: (_, e, s) => Container( ? Image.network(imageUrl, fit: BoxFit.cover, errorBuilder: (_, e, s) => Container(
width: 80, height: 60, width: 80, height: 60,
decoration: BoxDecoration(color: const Color(0xFFF0F0F0), borderRadius: BorderRadius.circular(8)), decoration: BoxDecoration(color: const Color(0xFFF0F0F0), borderRadius: BorderRadius.circular(8)),
child: const Icon(Icons.image, size: 24, color: Color(0xFFBBBBBB)), child: const Icon(Icons.image, size: 28, color: Color(0xFFBBBBBB)),
)) ))
: const SizedBox.shrink(), : const SizedBox.shrink(),
), ),
@@ -922,11 +923,11 @@ class ChatMessagesView extends ConsumerWidget {
Padding( Padding(
padding: const EdgeInsets.only(top: 10), padding: const EdgeInsets.only(top: 10),
child: Row(children: [ child: Row(children: [
const CircleAvatar(radius: 10, backgroundColor: Color(0xFFF0F2FF), child: Icon(Icons.chat_bubble_outline, size: 14, color: Color(0xFF8B9CF7))), const CircleAvatar(radius: 10, backgroundColor: AppTheme.primaryLight, child: Icon(Icons.chat_bubble_outline, size: 17, color: AppTheme.primary)),
const SizedBox(width: 6), const SizedBox(width: 6),
const Text('健康管家', style: TextStyle(fontSize: 12, color: Color(0xFF9E9E9E))), const Text('健康管家', style: TextStyle(fontSize: 15, color: Color(0xFF9E9E9E))),
const SizedBox(width: 4), const SizedBox(width: 4),
const Text('仅供参考', style: TextStyle(fontSize: 11, color: Color(0xFFCCCCCC))), const Text('仅供参考', style: TextStyle(fontSize: 14, color: Color(0xFFCCCCCC))),
]), ]),
), ),
], ],
@@ -943,16 +944,16 @@ class ChatMessagesView extends ConsumerWidget {
return ElevatedButton( return ElevatedButton(
onPressed: onTap ?? () {}, onPressed: onTap ?? () {},
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF6C5CE7), backgroundColor: AppTheme.primary,
foregroundColor: Colors.white, foregroundColor: Colors.white,
elevation: 0, elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
padding: const EdgeInsets.symmetric(vertical: 11), padding: const EdgeInsets.symmetric(vertical: 11),
), ),
child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [ child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [
Icon(icon, size: 16), Icon(icon, size: 19),
const SizedBox(width: 5), const SizedBox(width: 5),
Text(label, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500)), Text(label, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500)),
]), ]),
); );
} }
@@ -961,15 +962,15 @@ class ChatMessagesView extends ConsumerWidget {
return OutlinedButton( return OutlinedButton(
onPressed: onTap ?? () {}, onPressed: onTap ?? () {},
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
foregroundColor: const Color(0xFF6C5CE7), foregroundColor: AppTheme.primary,
side: const BorderSide(color: Color(0xFF6C5CE7), width: 1.2), side: const BorderSide(color: AppTheme.primary, width: 1.2),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
padding: const EdgeInsets.symmetric(vertical: 11), padding: const EdgeInsets.symmetric(vertical: 11),
), ),
child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [ child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [
Icon(icon, size: 15), Icon(icon, size: 18),
const SizedBox(width: 4), const SizedBox(width: 4),
Text(label, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500)), Text(label, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500)),
]), ]),
); );
} }
@@ -991,7 +992,7 @@ class ChatMessagesView extends ConsumerWidget {
: Image.file(File(path), fit: BoxFit.contain))), : Image.file(File(path), fit: BoxFit.contain))),
Positioned(top: 8, right: 8, child: GestureDetector( Positioned(top: 8, right: 8, child: GestureDetector(
onTap: () => Navigator.pop(ctx), onTap: () => Navigator.pop(ctx),
child: Container(padding: const EdgeInsets.all(6), decoration: BoxDecoration(color: Colors.white54, shape: BoxShape.circle), child: const Icon(Icons.close, size: 18)), child: Container(padding: const EdgeInsets.all(6), decoration: BoxDecoration(color: Colors.white54, shape: BoxShape.circle), child: const Icon(Icons.close, size: 21)),
)), )),
]), ]),
), ),
@@ -1069,7 +1070,7 @@ class ChatMessagesView extends ConsumerWidget {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text('打卡成功 已记录 ${reminders.length} 项服药'), content: Text('打卡成功 已记录 ${reminders.length} 项服药'),
backgroundColor: const Color(0xFF43A047), backgroundColor: AppTheme.success,
), ),
); );
} catch (e) { } catch (e) {
@@ -1260,13 +1261,13 @@ class ChatMessagesView extends ConsumerWidget {
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
boxShadow: [BoxShadow(color: const Color(0xFF8B9CF7).withAlpha(10), blurRadius: 8, offset: const Offset(0, 2))], boxShadow: [BoxShadow(color: AppTheme.primary.withAlpha(10), blurRadius: 8, offset: const Offset(0, 2))],
), ),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Row(children: [ Row(children: [
Icon(Icons.today, size: 18, color: const Color(0xFF8B9CF7)), Icon(Icons.today, size: 21, color: AppTheme.primary),
const SizedBox(width: 8), const SizedBox(width: 8),
const Text('今日任务', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))), const Text('今日任务', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
]), ]),
const SizedBox(height: 10), const SizedBox(height: 10),
...tasks, ...tasks,
@@ -1275,7 +1276,7 @@ class ChatMessagesView extends ConsumerWidget {
} }
Widget _taskRow(BuildContext context, IconData icon, String label, {String status = 'pending', String? trailing, VoidCallback? onTap}) { Widget _taskRow(BuildContext context, IconData icon, String label, {String status = 'pending', String? trailing, VoidCallback? onTap}) {
final colors = {'done': const Color(0xFF43A047), 'warning': const Color(0xFFFF9800), 'pending': const Color(0xFF9E9E9E), 'overdue': const Color(0xFFE53935)}; final colors = {'done': AppTheme.success, 'warning': const Color(0xFFFF9800), 'pending': const Color(0xFF9E9E9E), 'overdue': AppTheme.error};
final icons = {'done': Icons.check_circle, 'warning': Icons.warning, 'pending': Icons.circle_outlined, 'overdue': Icons.error}; final icons = {'done': Icons.check_circle, 'warning': Icons.warning, 'pending': Icons.circle_outlined, 'overdue': Icons.error};
final isOverdue = status == 'overdue'; final isOverdue = status == 'overdue';
return Padding( return Padding(
@@ -1290,10 +1291,10 @@ class ChatMessagesView extends ConsumerWidget {
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
child: Row(crossAxisAlignment: CrossAxisAlignment.center, children: [ child: Row(crossAxisAlignment: CrossAxisAlignment.center, children: [
Container(width: 30, height: 30, decoration: BoxDecoration(color: const Color(0xFFF0F2FF), borderRadius: BorderRadius.circular(8)), child: Icon(icon, size: 15, color: const Color(0xFF8B9CF7))), Container(width: 30, height: 30, decoration: BoxDecoration(color: AppTheme.primaryLight, borderRadius: BorderRadius.circular(8)), child: Icon(icon, size: 18, color: AppTheme.primary)),
const SizedBox(width: 10), const SizedBox(width: 10),
Expanded(child: Text(trailing ?? label, style: const TextStyle(fontSize: 13, color: Color(0xFF333333)))), Expanded(child: Text(trailing ?? label, style: const TextStyle(fontSize: 16, color: Color(0xFF333333)))),
Icon(icons[status] ?? Icons.circle_outlined, size: 18, color: colors[status] ?? Colors.grey), Icon(icons[status] ?? Icons.circle_outlined, size: 21, color: colors[status] ?? Colors.grey),
]), ]),
), ),
), ),
@@ -1391,16 +1392,16 @@ class _ExpandableAdviceState extends State<_ExpandableAdvice> {
children: [ children: [
Row( Row(
children: [ children: [
const Icon(Icons.lightbulb_outline, size: 16, color: Color(0xFF8B9CF7)), const Icon(Icons.lightbulb_outline, size: 19, color: AppTheme.primary),
const SizedBox(width: 6), const SizedBox(width: 6),
const Text('AI 建议', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: Color(0xFF8B9CF7))), const Text('AI 建议', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: AppTheme.primary)),
const Spacer(), const Spacer(),
Icon(_expanded ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down, size: 18, color: const Color(0xFFAAAAAA)), Icon(_expanded ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down, size: 21, color: const Color(0xFFAAAAAA)),
], ],
), ),
if (_expanded) ...[ if (_expanded) ...[
const SizedBox(height: 10), const SizedBox(height: 10),
Text(widget.advice, style: const TextStyle(fontSize: 13, color: Color(0xFF555555), height: 1.6)), Text(widget.advice, style: const TextStyle(fontSize: 16, color: Color(0xFF555555), height: 1.6)),
], ],
], ],
), ),

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../core/app_theme.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../providers/auth_provider.dart'; import '../../providers/auth_provider.dart';
import '../../providers/data_providers.dart'; import '../../providers/data_providers.dart';
@@ -33,13 +34,13 @@ class _MedicationCheckInPageState extends ConsumerState<MedicationCheckInPage> {
@override Widget build(BuildContext context) { @override Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFF8F9FC), backgroundColor: AppTheme.bg,
appBar: AppBar(title: const Text('服药打卡'), centerTitle: true), appBar: AppBar(title: const Text('服药打卡'), centerTitle: true),
body: FutureBuilder<List<Map<String, dynamic>>>( body: FutureBuilder<List<Map<String, dynamic>>>(
future: _future, future: _future,
builder: (ctx, snap) { builder: (ctx, snap) {
if (snap.connectionState == ConnectionState.waiting) { if (snap.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator(color: Color(0xFF8B9CF7))); return const Center(child: CircularProgressIndicator(color: AppTheme.primary));
} }
final reminders = snap.data ?? []; final reminders = snap.data ?? [];
if (reminders.isEmpty) { if (reminders.isEmpty) {
@@ -47,7 +48,7 @@ class _MedicationCheckInPageState extends ConsumerState<MedicationCheckInPage> {
child: Column(mainAxisSize: MainAxisSize.min, children: [ child: Column(mainAxisSize: MainAxisSize.min, children: [
Icon(Icons.check_circle_outline, size: 64, color: Colors.grey[300]), Icon(Icons.check_circle_outline, size: 64, color: Colors.grey[300]),
const SizedBox(height: 12), const SizedBox(height: 12),
const Text('今天所有用药已打卡', style: TextStyle(fontSize: 16, color: Color(0xFF999999))), const Text('今天所有用药已打卡', style: TextStyle(fontSize: 19, color: Color(0xFF999999))),
]), ]),
); );
} }
@@ -79,19 +80,19 @@ class _MedicationCheckInPageState extends ConsumerState<MedicationCheckInPage> {
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
boxShadow: [BoxShadow(color: const Color(0xFF8B9CF7).withAlpha(10), blurRadius: 8, offset: const Offset(0, 2))], boxShadow: [BoxShadow(color: AppTheme.primary.withAlpha(10), blurRadius: 8, offset: const Offset(0, 2))],
), ),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Row(children: [ Row(children: [
Container( Container(
width: 40, height: 40, width: 40, height: 40,
decoration: BoxDecoration(color: const Color(0xFFFFF3E0), borderRadius: BorderRadius.circular(10)), decoration: BoxDecoration(color: const Color(0xFFFFF3E0), borderRadius: BorderRadius.circular(10)),
child: const Center(child: Text('💊', style: TextStyle(fontSize: 20))), child: const Center(child: Text('💊', style: TextStyle(fontSize: 23))),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(medName, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700)), Text(medName, style: const TextStyle(fontSize: 19, fontWeight: FontWeight.w700)),
if (dosage.isNotEmpty) Text(dosage, style: const TextStyle(fontSize: 13, color: Color(0xFF888888))), if (dosage.isNotEmpty) Text(dosage, style: const TextStyle(fontSize: 16, color: Color(0xFF888888))),
]), ]),
]), ]),
const SizedBox(height: 14), const SizedBox(height: 14),
@@ -105,12 +106,12 @@ class _MedicationCheckInPageState extends ConsumerState<MedicationCheckInPage> {
child: Row(children: [ child: Row(children: [
Icon( Icon(
isTaken ? Icons.check_circle : Icons.radio_button_unchecked, isTaken ? Icons.check_circle : Icons.radio_button_unchecked,
size: 20, size: 23,
color: isTaken ? const Color(0xFF43A047) : const Color(0xFFCCCCCC), color: isTaken ? AppTheme.success : const Color(0xFFCCCCCC),
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
Text(time, style: TextStyle( Text(time, style: TextStyle(
fontSize: 15, fontWeight: FontWeight.w500, fontSize: 18, fontWeight: FontWeight.w500,
color: isTaken ? const Color(0xFF999999) : const Color(0xFF333333), color: isTaken ? const Color(0xFF999999) : const Color(0xFF333333),
)), )),
const Spacer(), const Spacer(),
@@ -119,12 +120,12 @@ class _MedicationCheckInPageState extends ConsumerState<MedicationCheckInPage> {
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isTaken ? const Color(0xFFDCFCE7) : const Color(0xFF8B9CF7), color: isTaken ? const Color(0xFFDCFCE7) : AppTheme.primary,
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
), ),
child: Text(isTaken ? '已打卡' : '打卡', child: Text(isTaken ? '已打卡' : '打卡',
style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600,
color: isTaken ? const Color(0xFF43A047) : Colors.white)), color: isTaken ? AppTheme.success : Colors.white)),
), ),
), ),
]), ]),

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../core/app_theme.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/navigation_provider.dart'; import '../../core/navigation_provider.dart';
import '../../providers/data_providers.dart'; import '../../providers/data_providers.dart';
@@ -56,8 +57,8 @@ class _MedicationEditPageState extends ConsumerState<MedicationEditPage> {
try { try {
if (widget.id != null) { await srv.update(widget.id!, data); } else { await srv.create(data); } if (widget.id != null) { await srv.update(widget.id!, data); } else { await srv.create(data); }
ref.invalidate(medicationListProvider); ref.invalidate(medicationReminderProvider); ref.invalidate(medicationListProvider); ref.invalidate(medicationReminderProvider);
if (mounted) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('已保存'), backgroundColor: Color(0xFF43A047))); popRoute(ref); } if (mounted) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('已保存'), backgroundColor: AppTheme.success)); popRoute(ref); }
} catch (_) { if (mounted) ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('保存失败'), backgroundColor: Color(0xFFE53935))); } } catch (_) { if (mounted) ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('保存失败'), backgroundColor: AppTheme.error)); }
if (mounted) setState(() => _loading = false); if (mounted) setState(() => _loading = false);
} }
@@ -71,7 +72,7 @@ class _MedicationEditPageState extends ConsumerState<MedicationEditPage> {
@override Widget build(BuildContext context) { @override Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFF8F9FC), backgroundColor: AppTheme.bg,
appBar: AppBar(title: Text(widget.id != null ? '编辑用药' : '添加用药')), appBar: AppBar(title: Text(widget.id != null ? '编辑用药' : '添加用药')),
body: ListView(padding: const EdgeInsets.all(16), children: [ body: ListView(padding: const EdgeInsets.all(16), children: [
// 名称+剂量 一行 // 名称+剂量 一行
@@ -93,8 +94,8 @@ class _MedicationEditPageState extends ConsumerState<MedicationEditPage> {
if (t != null) setState(() => _times[i] = t); if (t != null) setState(() => _times[i] = t);
}, },
child: Container(padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), child: Container(padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(color: const Color(0xFFEDEAFF), borderRadius: BorderRadius.circular(12)), 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: 15, color: Color(0xFF6C5CE7), fontWeight: FontWeight.w600))), 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))),
))), ))),
const SizedBox(height: 16), const SizedBox(height: 16),
// 开始+结束 一行 // 开始+结束 一行
@@ -106,30 +107,30 @@ class _MedicationEditPageState extends ConsumerState<MedicationEditPage> {
const SizedBox(height: 16), const SizedBox(height: 16),
_field('备注', _notesCtrl, hint: '如: 饭后服用、睡前'), _field('备注', _notesCtrl, hint: '如: 饭后服用、睡前'),
const SizedBox(height: 32), const SizedBox(height: 32),
SizedBox(width: double.infinity, child: ElevatedButton(onPressed: _loading ? null : _save, style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFF6C5CE7), foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), padding: const EdgeInsets.symmetric(vertical: 14)), child: Text(_loading ? '保存中...' : '保存', style: const TextStyle(fontSize: 16)))), 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)))),
const SizedBox(height: 20), const SizedBox(height: 20),
]), ]),
); );
} }
Widget _label(String text) => Text(text, style: const TextStyle(fontSize: 14, color: Color(0xFF666666))); Widget _label(String text) => Text(text, style: const TextStyle(fontSize: 17, color: Color(0xFF666666)));
Widget _field(String label, TextEditingController ctrl, {String? hint}) => Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Widget _field(String label, TextEditingController ctrl, {String? hint}) => Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
_label(label), const SizedBox(height: 6), _label(label), const SizedBox(height: 6),
TextField(controller: ctrl, decoration: InputDecoration(hintText: hint, filled: true, fillColor: Colors.white, border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none), contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12)), style: const TextStyle(fontSize: 16)), TextField(controller: ctrl, decoration: InputDecoration(hintText: hint, filled: true, fillColor: Colors.white, border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), 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 ? const Color(0xFF6C5CE7) : const Color(0xFFEDEAFF), borderRadius: BorderRadius.circular(16)), child: Text(label, style: TextStyle(fontSize: 13, color: sel ? Colors.white : const Color(0xFF6C5CE7), 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 ? 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 _dateField(String label, DateTime val, ValueChanged<DateTime> cb) => Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Widget _dateField(String label, DateTime val, ValueChanged<DateTime> cb) => Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
_label(label), const SizedBox(height: 6), _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); }, GestureDetector(onTap: () async { final d = await showDatePicker(context: context, initialDate: val, firstDate: DateTime(2024), lastDate: DateTime(2030)); if (d != null) cb(d); },
child: Container(width: double.infinity, padding: const EdgeInsets.all(12), decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12)), child: Text('${val.month}/${val.day}', style: const TextStyle(fontSize: 15)))), child: Container(width: double.infinity, padding: const EdgeInsets.all(12), decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12)), child: Text('${val.month}/${val.day}', style: const TextStyle(fontSize: 18)))),
]); ]);
Widget _dateFieldOpt(String label, DateTime? val, ValueChanged<DateTime?> cb) => Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Widget _dateFieldOpt(String label, DateTime? val, ValueChanged<DateTime?> cb) => Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
_label(label), const SizedBox(height: 6), _label(label), const SizedBox(height: 6),
GestureDetector(onTap: () async { final d = await showDatePicker(context: context, initialDate: val ?? DateTime.now(), firstDate: DateTime(2024), lastDate: DateTime(2030)); if (d != null) cb(d); }, GestureDetector(onTap: () async { final d = await showDatePicker(context: context, initialDate: val ?? DateTime.now(), firstDate: DateTime(2024), lastDate: DateTime(2030)); if (d != null) cb(d); },
child: Container(width: double.infinity, padding: const EdgeInsets.all(12), decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12)), child: Row(children: [ child: Container(width: double.infinity, padding: const EdgeInsets.all(12), decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12)), child: Row(children: [
Text(val != null ? '${val.month}/${val.day}' : '不设置', style: TextStyle(fontSize: 15, color: val != null ? null : Colors.grey[400])), Text(val != null ? '${val.month}/${val.day}' : '不设置', style: TextStyle(fontSize: 18, color: val != null ? null : Colors.grey[400])),
if (val != null) const Spacer(), if (val != null) const Spacer(),
if (val != null) GestureDetector(onTap: () => cb(null), child: const Icon(Icons.close, size: 16, color: Color(0xFFBBBBBB))), if (val != null) GestureDetector(onTap: () => cb(null), child: const Icon(Icons.close, size: 19, color: Color(0xFFBBBBBB))),
]))), ]))),
]); ]);
} }

View File

@@ -1,15 +1,21 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import '../../core/app_theme.dart';
import '../../core/navigation_provider.dart'; import '../../core/navigation_provider.dart';
import '../../providers/data_providers.dart'; import '../../providers/data_providers.dart';
import '../../widgets/app_empty_state.dart';
import '../../widgets/app_tab_chip.dart';
class MedicationListPage extends ConsumerStatefulWidget { class MedicationListPage extends ConsumerStatefulWidget {
const MedicationListPage({super.key}); const MedicationListPage({super.key});
@override ConsumerState<MedicationListPage> createState() => _MedicationListPageState(); @override ConsumerState<MedicationListPage> createState() => _MedicationListPageState();
} }
class _MedicationListPageState extends ConsumerState<MedicationListPage> { class _MedicationListPageState extends ConsumerState<MedicationListPage> {
String _filter = ''; String _filter = '';
Future<List<Map<String, dynamic>>>? _future; Future<List<Map<String, dynamic>>>? _future;
@override void initState() { super.initState(); _load(); } @override void initState() { super.initState(); _load(); }
void _load() { setState(() { _future = ref.read(medicationServiceProvider).getMedications(_filter); }); } void _load() { setState(() { _future = ref.read(medicationServiceProvider).getMedications(_filter); }); }
@@ -18,93 +24,116 @@ class _MedicationListPageState extends ConsumerState<MedicationListPage> {
_load(); _load();
} }
static const _tabs = [('全部', ''), ('服用中', 'active'), ('已停药', 'inactive')];
@override Widget build(BuildContext context) { @override Widget build(BuildContext context) {
final theme = ShadTheme.of(context);
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFF8F9FC), backgroundColor: theme.colorScheme.background,
appBar: AppBar(title: const Text('用药管理'), actions: [ appBar: AppBar(
TextButton(onPressed: () { pushRoute(ref, 'medicationEdit'); }, child: const Text('添加', style: TextStyle(fontSize: 15, color: Color(0xFF6C5CE7)))), title: const Text('用药管理'),
]), actions: [
ShadButton.ghost(
size: ShadButtonSize.sm,
child: const Text('添加'),
onPressed: () => pushRoute(ref, 'medicationEdit'),
),
],
),
body: Column(children: [ body: Column(children: [
Padding(padding: const EdgeInsets.all(12), child: Row(children: [ Padding(
_tab('全部', ''), _tab('服用中', 'active'), _tab('已停药', 'inactive'), 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>>>( Expanded(child: FutureBuilder<List<Map<String, dynamic>>>(
future: _future, future: _future,
builder: (ctx, snap) { builder: (ctx, snap) {
final list = snap.data ?? []; final list = snap.data ?? [];
if (list.isEmpty) return Center(child: Column(mainAxisSize: MainAxisSize.min, children: [ if (list.isEmpty) {
Icon(Icons.medication_outlined, size: 64, color: Colors.grey[300]), return AppEmptyState(
const SizedBox(height: 12), Text('暂无用药', style: TextStyle(color: Colors.grey[500])), icon: LucideIcons.pill,
])); title: '暂无用药',
return ListView.builder(padding: const EdgeInsets.fromLTRB(0, 0, 0, 12), itemCount: list.length + 1, itemBuilder: (ctx, i) { subtitle: '点击右上角添加药品',
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(' ') ?? ''; return ListView.builder(
final isActive = m['isActive'] == true; padding: const EdgeInsets.fromLTRB(0, 0, 0, AppTheme.sMd),
return _SwipeDeleteTile( itemCount: list.length + 1,
key: Key(m['id']?.toString() ?? '$i'), itemBuilder: (ctx, i) {
onDelete: () => _delete(m['id']?.toString() ?? ''), if (i == list.length) return const SizedBox(height: 80);
onTap: () => pushRoute(ref, 'medCheckIn', params: {'id': m['id']?.toString() ?? ''}), final m = list[i];
child: Container( final times = (m['timeOfDay'] as List?)?.map((t) => t.toString().substring(0, 5)).join(' ') ?? '';
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), final isActive = m['isActive'] == true;
padding: const EdgeInsets.all(14), final id = m['id']?.toString() ?? '';
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16)), return Dismissible(
child: Row(children: [ key: Key(id),
Container(width: 44, height: 44, decoration: BoxDecoration(color: isActive ? const Color(0xFFEDEAFF) : const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(12)), child: Icon(Icons.medication_outlined, size: 22, color: isActive ? const Color(0xFF6C5CE7) : Colors.grey)), direction: DismissDirection.endToStart,
const SizedBox(width: 12), confirmDismiss: (_) async {
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ await _delete(id);
Row(children: [ return false;
Text(m['name']?.toString() ?? '', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: isActive ? const Color(0xFF1A1A1A) : Colors.grey)), },
if (!isActive) ...[const SizedBox(width: 6), Container(padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1), decoration: BoxDecoration(color: Colors.grey[200], borderRadius: BorderRadius.circular(4)), child: const Text('已停', style: TextStyle(fontSize: 10, color: Color(0xFF999999))))], background: Container(
margin: const EdgeInsets.symmetric(horizontal: AppTheme.sLg, vertical: 4),
decoration: BoxDecoration(
color: theme.colorScheme.destructive,
borderRadius: BorderRadius.circular(AppTheme.rMd),
),
alignment: Alignment.centerRight,
padding: const EdgeInsets.only(right: 20),
child: Icon(LucideIcons.trash, color: theme.colorScheme.destructiveForeground, size: 25),
),
child: GestureDetector(
onTap: () => pushRoute(ref, 'medCheckIn', params: {'id': id}),
child: Container(
margin: const EdgeInsets.symmetric(horizontal: AppTheme.sLg, vertical: 4),
padding: const EdgeInsets.all(AppTheme.sLg),
decoration: BoxDecoration(
color: theme.colorScheme.card,
borderRadius: BorderRadius.circular(AppTheme.rMd),
boxShadow: [AppTheme.shadowLight],
),
child: Row(children: [
Container(
width: 44, height: 44,
decoration: BoxDecoration(
color: isActive ? AppTheme.primaryLight : theme.colorScheme.muted,
borderRadius: BorderRadius.circular(AppTheme.rSm),
),
child: Icon(LucideIcons.pill, size: 25, color: isActive ? AppTheme.primary : theme.colorScheme.mutedForeground),
),
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 ? theme.colorScheme.foreground : theme.colorScheme.mutedForeground)),
if (!isActive) ...[
const SizedBox(width: 6),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1),
decoration: BoxDecoration(
color: theme.colorScheme.muted,
borderRadius: BorderRadius.circular(4),
),
child: Text('已停', style: TextStyle(fontSize: 13, color: theme.colorScheme.mutedForeground)),
),
],
]),
const SizedBox(height: 2),
Text('${m['dosage'] ?? ''} $times', style: TextStyle(fontSize: 16, color: theme.colorScheme.mutedForeground)),
])),
Icon(LucideIcons.chevronRight, size: 21, color: theme.colorScheme.border),
]), ]),
const SizedBox(height: 2), ),
Text('${m['dosage'] ?? ''} $times', style: const TextStyle(fontSize: 13, color: Color(0xFF888888))), ),
])), );
const Icon(Icons.chevron_right, size: 20, color: Color(0xFFCCCCCC)), },
]), );
),
);
});
}, },
)), )),
]), ]),
); );
} }
Widget _tab(String label, String value) {
final sel = _filter == value;
return GestureDetector(onTap: () { _filter = value; _load(); },
child: Container(margin: const EdgeInsets.only(right: 8), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
decoration: BoxDecoration(color: sel ? const Color(0xFF6C5CE7) : const Color(0xFFEDEAFF), borderRadius: BorderRadius.circular(16)),
child: Text(label, style: TextStyle(fontSize: 13, color: sel ? Colors.white : const Color(0xFF6C5CE7), fontWeight: FontWeight.w500))),
);
}
}
class _SwipeDeleteTile extends StatefulWidget {
final Widget child; final VoidCallback onDelete; final VoidCallback onTap;
const _SwipeDeleteTile({super.key, required this.child, required this.onDelete, required this.onTap});
@override State<_SwipeDeleteTile> createState() => _SwipeDeleteTileState();
}
class _SwipeDeleteTileState extends State<_SwipeDeleteTile> with SingleTickerProviderStateMixin {
double _dx = 0; static const _max = 80.0, _threshold = 40.0;
void _onUpdate(DragUpdateDetails d) => setState(() => _dx = (_dx + d.delta.dx).clamp(-_max, 0.0));
void _onEnd(DragEndDetails d) => setState(() => _dx = _dx < -_threshold ? -_max : 0.0);
void _doDelete() { widget.onDelete(); setState(() => _dx = 0); }
@override Widget build(BuildContext context) {
final w = GestureDetector(
onHorizontalDragUpdate: _onUpdate,
onHorizontalDragEnd: _onEnd,
child: Transform.translate(offset: Offset(_dx, 0), child: widget.child),
);
final red = Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
decoration: const BoxDecoration(color: Color(0xFFE53935), borderRadius: BorderRadius.all(Radius.circular(16))),
child: const Align(alignment: Alignment.centerRight, child: Padding(padding: EdgeInsets.only(right: 20), child: Icon(Icons.delete_outline, color: Colors.white, size: 24))),
);
return GestureDetector(
onTap: _dx < 0 ? _doDelete : widget.onTap,
child: Stack(children: [Positioned.fill(child: red), w]),
);
}
} }

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../core/app_theme.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/navigation_provider.dart'; import '../../core/navigation_provider.dart';
import '../../providers/data_providers.dart'; import '../../providers/data_providers.dart';
@@ -12,19 +13,19 @@ class ProfileDetailPage extends ConsumerWidget {
final userService = ref.watch(userServiceProvider); final userService = ref.watch(userServiceProvider);
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFF8F9FC), backgroundColor: AppTheme.bg,
appBar: AppBar( appBar: AppBar(
backgroundColor: Colors.white, backgroundColor: Colors.white,
elevation: 0, elevation: 0,
leading: IconButton(icon: const Icon(Icons.chevron_left), onPressed: () => Navigator.pop(context)), leading: IconButton(icon: const Icon(Icons.chevron_left), onPressed: () => Navigator.pop(context)),
title: Row(mainAxisSize: MainAxisSize.min, children: [ title: Row(mainAxisSize: MainAxisSize.min, children: [
Icon(Icons.person_outline, size: 20, color: Colors.grey[600]), Icon(Icons.person_outline, size: 23, color: Colors.grey[600]),
const SizedBox(width: 6), const SizedBox(width: 6),
Text('健康档案', style: TextStyle(color: Colors.grey[800], fontWeight: FontWeight.w600)), Text('健康档案', style: TextStyle(color: Colors.grey[800], fontWeight: FontWeight.w600)),
]), ]),
centerTitle: true, centerTitle: true,
), ),
body: SafeArea(child: SingleChildScrollView(padding: const EdgeInsets.all(16), child: Column(children: [_buildUserCard(userService), const SizedBox(height: 16), _buildHealthOverview(latestHealth), const SizedBox(height: 16), _buildHistoryList(), const SizedBox(height: 16), SizedBox(width: double.infinity, height: 48, child: OutlinedButton(onPressed: () => pushRoute(ref, 'settings'), style: OutlinedButton.styleFrom(foregroundColor: const Color(0xFF8B9CF7), side: const BorderSide(color: Color(0xFF8B9CF7)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24))), child: const Text('退出档案')))]))), body: SafeArea(child: SingleChildScrollView(padding: const EdgeInsets.all(16), child: Column(children: [_buildUserCard(userService), const SizedBox(height: 16), _buildHealthOverview(latestHealth), const SizedBox(height: 16), _buildHistoryList(), const SizedBox(height: 16), SizedBox(width: double.infinity, height: 48, child: OutlinedButton(onPressed: () => pushRoute(ref, 'settings'), style: OutlinedButton.styleFrom(foregroundColor: AppTheme.primary, side: const BorderSide(color: AppTheme.primary), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24))), child: const Text('退出档案')))]))),
); );
} }
@@ -41,17 +42,17 @@ class ProfileDetailPage extends ConsumerWidget {
return Container( return Container(
width: double.infinity, padding: const EdgeInsets.all(20), width: double.infinity, padding: const EdgeInsets.all(20),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20), decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20),
boxShadow: [BoxShadow(color: const Color(0xFF8B9CF7).withAlpha(10), blurRadius: 8, offset: const Offset(0, 2))]), boxShadow: [BoxShadow(color: AppTheme.primary.withAlpha(10), blurRadius: 8, offset: const Offset(0, 2))]),
child: Row(children: [ child: Row(children: [
CircleAvatar(radius: 32, backgroundColor: const Color(0xFFF0F2FF), CircleAvatar(radius: 32, backgroundColor: AppTheme.primaryLight,
child: Text(name.toString().isNotEmpty ? name.toString()[0] : '?', style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: Color(0xFF8B9CF7)))), child: Text(name.toString().isNotEmpty ? name.toString()[0] : '?', style: const TextStyle(fontSize: 25, fontWeight: FontWeight.bold, color: AppTheme.primary))),
const SizedBox(width: 16), const SizedBox(width: 16),
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(name.toString(), style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: Color(0xFF1A1A1A))), Text(name.toString(), style: const TextStyle(fontSize: 25, fontWeight: FontWeight.bold, color: Color(0xFF1A1A1A))),
if (info.isNotEmpty) const SizedBox(height: 4), if (info.isNotEmpty) const SizedBox(height: 4),
if (info.isNotEmpty) Text(info, style: TextStyle(fontSize: 14, color: Colors.grey[500])), if (info.isNotEmpty) Text(info, style: TextStyle(fontSize: 17, color: Colors.grey[500])),
])), ])),
Icon(Icons.chevron_right, size: 24, color: Colors.grey[400]), Icon(Icons.chevron_right, size: 28, color: Colors.grey[400]),
]), ]),
); );
}, },
@@ -71,13 +72,13 @@ class ProfileDetailPage extends ConsumerWidget {
return Container( return Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20), boxShadow: [BoxShadow(color: const Color(0xFF8B9CF7).withAlpha(10), blurRadius: 8, offset: const Offset(0, 2))]), decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20), boxShadow: [BoxShadow(color: AppTheme.primary.withAlpha(10), blurRadius: 8, offset: const Offset(0, 2))]),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
const Text('健康概览', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))), const Text('健康概览', style: TextStyle(fontSize: 21, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
const SizedBox(height: 4), const SizedBox(height: 4),
Text('(最近测量)', style: TextStyle(fontSize: 13, color: Colors.grey[500])), Text('(最近测量)', style: TextStyle(fontSize: 16, color: Colors.grey[500])),
const SizedBox(height: 16), const SizedBox(height: 16),
healthData.when(data: (data) => _buildMetricsList(data), loading: () => const Center(child: Padding(padding: EdgeInsets.all(24), child: CircularProgressIndicator(strokeWidth: 2, color: Color(0xFF8B9CF7)))), error: (_, e) => _buildMetricsEmpty()), healthData.when(data: (data) => _buildMetricsList(data), loading: () => const Center(child: Padding(padding: EdgeInsets.all(24), child: CircularProgressIndicator(strokeWidth: 2, color: AppTheme.primary))), error: (_, e) => _buildMetricsEmpty()),
]), ]),
); );
} }
@@ -93,15 +94,15 @@ class ProfileDetailPage extends ConsumerWidget {
String _formatBP(dynamic bp) { if (bp is Map) { final s = bp['systolic']; final d = bp['diastolic']; if (s != null && d != null) return '$s/$d'; } return '--/--'; } String _formatBP(dynamic bp) { if (bp is Map) { final s = bp['systolic']; final d = bp['diastolic']; if (s != null && d != null) return '$s/$d'; } return '--/--'; }
String _formatMetric(dynamic val, String unit) { if (val is Map) { final v = val['value']; if (v != null) return '$v$unit'; } return '-- $unit'; } String _formatMetric(dynamic val, String unit) { if (val is Map) { final v = val['value']; if (v != null) return '$v$unit'; } return '-- $unit'; }
Widget _metricRow(IconData icon, String label, String value) => InkWell(onTap: () {}, borderRadius: BorderRadius.circular(12), child: Padding(padding: const EdgeInsets.symmetric(vertical: 14), child: Row(children: [Container(width: 40, height: 40, decoration: BoxDecoration(color: const Color(0xFFF0F2FF), borderRadius: BorderRadius.circular(10)), child: Icon(icon, size: 18, color: const Color(0xFF8B9CF7))), const SizedBox(width: 12), Expanded(child: Text(label, style: const TextStyle(fontSize: 15, color: Color(0xFF333333)))), Text(value, style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))), const SizedBox(width: 8), Icon(Icons.chevron_right, size: 18, color: Colors.grey[400])]))); Widget _metricRow(IconData icon, String label, String value) => InkWell(onTap: () {}, borderRadius: BorderRadius.circular(12), child: Padding(padding: const EdgeInsets.symmetric(vertical: 14), child: Row(children: [Container(width: 40, height: 40, decoration: BoxDecoration(color: AppTheme.primaryLight, borderRadius: BorderRadius.circular(10)), child: Icon(icon, size: 21, color: AppTheme.primary)), const SizedBox(width: 12), Expanded(child: Text(label, style: const TextStyle(fontSize: 18, color: Color(0xFF333333)))), Text(value, style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))), const SizedBox(width: 8), Icon(Icons.chevron_right, size: 21, color: Colors.grey[400])])));
Widget _buildHistoryList() { Widget _buildHistoryList() {
final items = [{'date': '05-31', 'label': '血压 · 餐前', 'value': '128/82', 'status': 'normal'}, {'date': '05-30', 'label': '血压 · 餐后', 'value': '135/85', 'status': 'warning'}, {'date': '05-29', 'label': '血压 · 餐前', 'value': '122/78', 'status': 'normal'}, {'date': '05-28', 'label': '血压 · 餐前', 'value': '118/76', 'status': 'normal'}, {'date': '05-27', 'label': '血糖 · 空腹', 'value': '5.6', 'status': 'normal'}, {'date': '05-26', 'label': '血压 · 餐前', 'value': '120/80', 'status': 'normal'}]; final items = [{'date': '05-31', 'label': '血压 · 餐前', 'value': '128/82', 'status': 'normal'}, {'date': '05-30', 'label': '血压 · 餐后', 'value': '135/85', 'status': 'warning'}, {'date': '05-29', 'label': '血压 · 餐前', 'value': '122/78', 'status': 'normal'}, {'date': '05-28', 'label': '血压 · 餐前', 'value': '118/76', 'status': 'normal'}, {'date': '05-27', 'label': '血糖 · 空腹', 'value': '5.6', 'status': 'normal'}, {'date': '05-26', 'label': '血压 · 餐前', 'value': '120/80', 'status': 'normal'}];
return Container(decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20), boxShadow: [BoxShadow(color: const Color(0xFF8B9CF7).withAlpha(10), blurRadius: 8, offset: const Offset(0, 2))]), child: Column(children: [Container(padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), child: Row(children: [const Text('历史记录', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))), const Spacer(), Text('查看更多', style: TextStyle(fontSize: 13, color: const Color(0xFF8B9CF7)))])), ...items.map(_historyItem)])); return Container(decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20), boxShadow: [BoxShadow(color: AppTheme.primary.withAlpha(10), blurRadius: 8, offset: const Offset(0, 2))]), child: Column(children: [Container(padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), child: Row(children: [const Text('历史记录', style: TextStyle(fontSize: 21, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))), const Spacer(), Text('查看更多', style: TextStyle(fontSize: 16, color: AppTheme.primary))])), ...items.map(_historyItem)]));
} }
Widget _historyItem(Map<String, dynamic> item) { Widget _historyItem(Map<String, dynamic> item) {
final isNormal = item['status'] == 'normal'; final isNormal = item['status'] == 'normal';
return Container(margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration(borderRadius: BorderRadius.circular(12)), child: Row(children: [Text(item['date']?.toString() ?? '', style: const TextStyle(fontSize: 14, color: Color(0xFF9E9E9E))), const SizedBox(width: 8), Expanded(child: Text(item['label']?.toString() ?? '', style: const TextStyle(fontSize: 14, color: Color(0xFF333333)))), Text(item['value']?.toString() ?? '', style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))), const SizedBox(width: 8), Container(padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration(color: isNormal ? const Color(0xFF43A047).withAlpha(20) : const Color(0xFFFF9800).withAlpha(20), borderRadius: BorderRadius.circular(10)), child: Text(isNormal ? '正常' : '偏高', style: TextStyle(fontSize: 11, color: isNormal ? const Color(0xFF43A047) : const Color(0xFFFF9800))))])); return Container(margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration(borderRadius: BorderRadius.circular(12)), child: Row(children: [Text(item['date']?.toString() ?? '', style: const TextStyle(fontSize: 17, color: Color(0xFF9E9E9E))), const SizedBox(width: 8), Expanded(child: Text(item['label']?.toString() ?? '', style: const TextStyle(fontSize: 17, color: Color(0xFF333333)))), Text(item['value']?.toString() ?? '', style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))), const SizedBox(width: 8), Container(padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration(color: isNormal ? AppTheme.success.withAlpha(20) : const Color(0xFFFF9800).withAlpha(20), borderRadius: BorderRadius.circular(10)), child: Text(isNormal ? '正常' : '偏高', style: TextStyle(fontSize: 14, color: isNormal ? AppTheme.success : const Color(0xFFFF9800))))]));
} }
} }

View File

@@ -1,7 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import '../../core/app_theme.dart';
import '../../core/navigation_provider.dart'; import '../../core/navigation_provider.dart';
import '../../providers/auth_provider.dart'; import '../../providers/auth_provider.dart';
import '../../widgets/app_menu_item.dart';
class ProfilePage extends ConsumerWidget { class ProfilePage extends ConsumerWidget {
const ProfilePage({super.key}); const ProfilePage({super.key});
@@ -9,100 +12,101 @@ class ProfilePage extends ConsumerWidget {
@override Widget build(BuildContext context, WidgetRef ref) { @override Widget build(BuildContext context, WidgetRef ref) {
final auth = ref.watch(authProvider); final auth = ref.watch(authProvider);
final user = auth.user; final user = auth.user;
final theme = ShadTheme.of(context);
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFF8F9FC), backgroundColor: theme.colorScheme.background,
body: SafeArea(child: SingleChildScrollView(padding: const EdgeInsets.only(bottom: 20), child: Column(children: [ body: SafeArea(child: SingleChildScrollView(
// 用户头部 padding: const EdgeInsets.only(bottom: AppTheme.sXl),
Container( child: Column(children: [
width: double.infinity, // 用户头部卡片
padding: const EdgeInsets.all(24), Container(
decoration: const BoxDecoration( width: double.infinity,
color: Colors.white, padding: const EdgeInsets.all(AppTheme.rXl),
borderRadius: BorderRadius.only(bottomLeft: Radius.circular(24), bottomRight: Radius.circular(24)), decoration: BoxDecoration(
), color: theme.colorScheme.card,
child: InkWell( borderRadius: const BorderRadius.only(
onTap: () => pushRoute(ref, 'healthArchive'), bottomLeft: Radius.circular(AppTheme.rXl),
borderRadius: BorderRadius.circular(12), bottomRight: Radius.circular(AppTheme.rXl),
child: Padding( ),
padding: const EdgeInsets.symmetric(vertical: 8), boxShadow: [AppTheme.shadowLight],
child: Row(children: [ ),
Stack(children: [ child: InkWell(
CircleAvatar(radius: 32, backgroundColor: const Color(0xFFEDEAFF), onTap: () => pushRoute(ref, 'healthArchive'),
backgroundImage: user?.avatarUrl != null ? NetworkImage(user!.avatarUrl!) : null, borderRadius: BorderRadius.circular(AppTheme.rSm),
child: user?.avatarUrl == null ? const Icon(Icons.person, size: 36, color: Color(0xFF6C5CE7)) : null), child: Padding(
Positioned(right: 0, bottom: 0, padding: const EdgeInsets.symmetric(vertical: AppTheme.sSm),
child: Container(width: 20, height: 20, child: Row(children: [
decoration: BoxDecoration(color: const Color(0xFF6C5CE7), borderRadius: BorderRadius.circular(10), border: Border.all(color: Colors.white, width: 2)), Stack(children: [
child: const Icon(Icons.edit, size: 10, color: Colors.white))), CircleAvatar(
radius: 32,
backgroundColor: AppTheme.primaryLight,
backgroundImage: user?.avatarUrl != null ? NetworkImage(user!.avatarUrl!) : null,
child: user?.avatarUrl == null ? const Icon(Icons.person, size: 36, color: AppTheme.primary) : null,
),
Positioned(
right: 0, bottom: 0,
child: Container(
width: 20, height: 20,
decoration: BoxDecoration(
color: AppTheme.primary,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: theme.colorScheme.card, width: 2),
),
child: const Icon(Icons.edit, size: 13, color: Colors.white),
),
),
]),
const SizedBox(width: AppTheme.sLg),
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(user?.name ?? '未设置昵称',
style: TextStyle(fontSize: 23, fontWeight: FontWeight.bold, color: theme.colorScheme.foreground)),
const SizedBox(height: 4),
Text(user?.phone ?? '', style: TextStyle(fontSize: 17, color: theme.colorScheme.mutedForeground)),
])),
Icon(LucideIcons.chevronRight, size: 25, color: theme.colorScheme.mutedForeground),
]), ]),
const SizedBox(width: 16), ),
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(user?.name ?? '未设置昵称', style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Color(0xFF1A1A1A))),
const SizedBox(height: 4),
Text(user?.phone ?? '', style: TextStyle(fontSize: 14, color: Colors.grey[500])),
])),
Icon(Icons.chevron_right, size: 24, color: Colors.grey[400]),
]),
), ),
), ),
), const SizedBox(height: AppTheme.sMd),
const SizedBox(height: 12), AppMenuItem(icon: LucideIcons.folderArchive, title: '健康档案', onTap: () => pushRoute(ref, 'healthArchive')),
_MenuItem(icon: Icons.folder_shared, title: '健康档案', onTap: () => pushRoute(ref, 'healthArchive')), AppMenuItem(icon: LucideIcons.monitorSmartphone, title: '设备管理', onTap: () => pushRoute(ref, 'devices')),
_MenuItem(icon: Icons.devices, title: '备管理', onTap: () => pushRoute(ref, 'devices')), AppMenuItem(icon: LucideIcons.settings, title: '', onTap: () => pushRoute(ref, 'settings')),
_MenuItem(icon: Icons.settings_outlined, title: '设置', onTap: () => pushRoute(ref, 'settings')), const SizedBox(height: 40),
// 退出登录
const SizedBox(height: 40), GestureDetector(
GestureDetector( onTap: () => _logout(context, ref),
onTap: () async { child: Container(
final ok = await showDialog<bool>( margin: const EdgeInsets.symmetric(horizontal: AppTheme.rXl),
context: context, height: 50,
builder: (ctx) => AlertDialog( alignment: Alignment.center,
title: const Text('退出登录'), decoration: BoxDecoration(
content: const Text('确定退出?'), border: Border.all(color: theme.colorScheme.destructive.withAlpha(80)),
actions: [ borderRadius: BorderRadius.circular(AppTheme.rPill),
TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('取消')),
TextButton(onPressed: () => Navigator.pop(ctx, true), child: const Text('确定')),
],
), ),
); child: Text('退出登录', style: TextStyle(fontSize: 19, color: theme.colorScheme.destructive, fontWeight: FontWeight.w500)),
if (ok == true) { await ref.read(authProvider.notifier).logout(); goRoute(ref, 'login'); } ),
},
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 24),
height: 50, alignment: Alignment.center,
decoration: BoxDecoration(border: Border.all(color: const Color(0xFFE53935)), borderRadius: BorderRadius.circular(25)),
child: const Text('退出登录', style: TextStyle(fontSize: 16, color: Color(0xFFE53935), fontWeight: FontWeight.w500)),
), ),
), ]),
]))), )),
); );
} }
}
class _MenuItem extends StatelessWidget { Future<void> _logout(BuildContext context, WidgetRef ref) async {
final IconData icon; final theme = ShadTheme.of(context);
final String title; final ok = await showDialog<bool>(
final String? trailing; context: context,
final VoidCallback? onTap; builder: (ctx) => AlertDialog(
const _MenuItem({required this.icon, required this.title, this.trailing, this.onTap}); shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppTheme.rXl)),
title: Text('退出登录', style: TextStyle(color: theme.colorScheme.foreground)),
@override Widget build(BuildContext context) { content: Text('确定退出?', style: TextStyle(color: theme.colorScheme.mutedForeground)),
return GestureDetector( actions: [
onTap: onTap, TextButton(onPressed: () => Navigator.pop(ctx, false), child: Text('取消', style: TextStyle(color: theme.colorScheme.mutedForeground))),
behavior: HitTestBehavior.opaque, TextButton(onPressed: () => Navigator.pop(ctx, true), child: Text('确定', style: TextStyle(color: theme.colorScheme.destructive))),
child: Container( ],
margin: const EdgeInsets.symmetric(horizontal: 24, vertical: 2),
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 14),
decoration: const BoxDecoration(color: Colors.white),
child: Row(children: [
Container(width: 36, height: 36, decoration: BoxDecoration(color: const Color(0xFFEDEAFF), borderRadius: BorderRadius.circular(10)), child: Icon(icon, size: 18, color: const Color(0xFF6C5CE7))),
const SizedBox(width: 12),
Expanded(child: Text(title, style: const TextStyle(fontSize: 16, color: Color(0xFF1A1A1A)))),
if (trailing != null && trailing!.isNotEmpty) Text(trailing!, style: TextStyle(fontSize: 14, color: Colors.grey[400])),
Icon(Icons.chevron_right, size: 20, color: Colors.grey[300]),
]),
), ),
); );
if (ok == true) { await ref.read(authProvider.notifier).logout(); goRoute(ref, 'login'); }
} }
} }

View File

@@ -21,7 +21,7 @@ class ServicePackageDetailPage extends ConsumerWidget {
return Scaffold( return Scaffold(
backgroundColor: AppTheme.bg, backgroundColor: AppTheme.bg,
appBar: AppBar( appBar: AppBar(
title: Text(package.title, style: const TextStyle(fontSize: 16)), title: Text(package.title, style: const TextStyle(fontSize: 19)),
centerTitle: true, centerTitle: true,
), ),
body: SingleChildScrollView( body: SingleChildScrollView(
@@ -50,12 +50,12 @@ class ServicePackageDetailPage extends ConsumerWidget {
color: Colors.white.withAlpha(40), color: Colors.white.withAlpha(40),
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(6),
), ),
child: const Text('VIP 产品权益', style: TextStyle(fontSize: 12, color: Colors.white, fontWeight: FontWeight.w600)), child: const Text('VIP 产品权益', style: TextStyle(fontSize: 15, color: Colors.white, fontWeight: FontWeight.w600)),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
Text(package.title, style: const TextStyle(fontSize: 22, fontWeight: FontWeight.w700, color: Colors.white)), Text(package.title, style: const TextStyle(fontSize: 25, fontWeight: FontWeight.w700, color: Colors.white)),
const SizedBox(height: 8), const SizedBox(height: 8),
Text(package.subtitle, style: TextStyle(fontSize: 14, color: Colors.white.withAlpha(200))), Text(package.subtitle, style: TextStyle(fontSize: 17, color: Colors.white.withAlpha(200))),
], ],
), ),
), ),
@@ -76,19 +76,19 @@ class ServicePackageDetailPage extends ConsumerWidget {
color: AppTheme.primaryLight, color: AppTheme.primaryLight,
borderRadius: BorderRadius.circular(14), borderRadius: BorderRadius.circular(14),
), ),
child: Icon(s.icon, size: 22, color: AppTheme.primary), child: Icon(s.icon, size: 25, color: AppTheme.primary),
), ),
const SizedBox(height: 6), const SizedBox(height: 6),
Text(s.label, style: const TextStyle(fontSize: 12, color: AppTheme.textSub), textAlign: TextAlign.center), Text(s.label, style: const TextStyle(fontSize: 15, color: AppTheme.textSub), textAlign: TextAlign.center),
], ],
), ),
)).toList(), )).toList(),
), ),
), ),
// 适用人群 // 适用人群
_Section(title: '适用人群', child: Text(package.targetAudience, style: const TextStyle(fontSize: 14, color: AppTheme.textSub, height: 1.6))), _Section(title: '适用人群', child: Text(package.targetAudience, style: const TextStyle(fontSize: 17, color: AppTheme.textSub, height: 1.6))),
// 详细说明 // 详细说明
...package.detailSections.map((s) => _Section(title: s.title, child: Text(s.content, style: const TextStyle(fontSize: 14, color: AppTheme.textSub, height: 1.6)))), ...package.detailSections.map((s) => _Section(title: s.title, child: Text(s.content, style: const TextStyle(fontSize: 17, color: AppTheme.textSub, height: 1.6)))),
const SizedBox(height: 40), const SizedBox(height: 40),
], ],
), ),
@@ -115,7 +115,7 @@ class _Section extends StatelessWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: AppTheme.text)), Text(title, style: const TextStyle(fontSize: 19, fontWeight: FontWeight.w600, color: AppTheme.text)),
const SizedBox(height: 10), const SizedBox(height: 10),
child, child,
], ],

View File

@@ -1,5 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import '../core/app_theme.dart';
import '../core/navigation_provider.dart'; import '../core/navigation_provider.dart';
import '../providers/auth_provider.dart'; import '../providers/auth_provider.dart';
import '../providers/data_providers.dart'; import '../providers/data_providers.dart';
@@ -17,12 +19,12 @@ class _DietRecordListPageState extends ConsumerState<DietRecordListPage> {
@override Widget build(BuildContext context) { @override Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFF8F9FC), backgroundColor: AppTheme.bg,
appBar: AppBar(title: const Text('饮食记录')), appBar: AppBar(title: const Text('饮食记录')),
body: FutureBuilder<List<Map<String, dynamic>>>( body: FutureBuilder<List<Map<String, dynamic>>>(
future: _future, future: _future,
builder: (ctx, snap) { builder: (ctx, snap) {
if (snap.connectionState == ConnectionState.waiting) return const Center(child: CircularProgressIndicator(color: Color(0xFF6C5CE7))); if (snap.connectionState == ConnectionState.waiting) return const Center(child: CircularProgressIndicator(color: AppTheme.primary));
final data = snap.data ?? []; final data = snap.data ?? [];
if (data.isEmpty) return _empty(context, '饮食记录', '暂无饮食记录,可通过「拍饮食」录入'); if (data.isEmpty) return _empty(context, '饮食记录', '暂无饮食记录,可通过「拍饮食」录入');
return ListView.builder( return ListView.builder(
@@ -46,18 +48,18 @@ class _DietRecordListPageState extends ConsumerState<DietRecordListPage> {
padding: const EdgeInsets.all(14), padding: const EdgeInsets.all(14),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16)), decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16)),
child: Row(children: [ 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)))), Container(width: 44, height: 44, decoration: BoxDecoration(color: AppTheme.primaryLight, borderRadius: BorderRadius.circular(12)), child: Center(child: Text(mealIcons[d['mealType']?.toString()] ?? '🍽️', style: const TextStyle(fontSize: 23)))),
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Row(children: [ Row(children: [
Text(mealLabel, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600)), Text(mealLabel, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600)),
const SizedBox(width: 8), const SizedBox(width: 8),
Text('${d['totalCalories'] ?? 0}千卡', style: const TextStyle(fontSize: 13, color: Color(0xFF6C5CE7))), Text('${d['totalCalories'] ?? 0}千卡', style: const TextStyle(fontSize: 16, color: AppTheme.primary)),
]), ]),
const SizedBox(height: 4), const SizedBox(height: 4),
Text(items.map((f) => f['name']).join(' · '), style: const TextStyle(fontSize: 13, color: Color(0xFF888888)), maxLines: 1, overflow: TextOverflow.ellipsis), Text(items.map((f) => f['name']).join(' · '), style: const TextStyle(fontSize: 16, color: Color(0xFF888888)), maxLines: 1, overflow: TextOverflow.ellipsis),
])), ])),
const Icon(Icons.chevron_right, size: 18, color: Color(0xFFCCCCCC)), const Icon(Icons.chevron_right, size: 21, color: Color(0xFFCCCCCC)),
]), ]),
), ),
); );
@@ -76,12 +78,12 @@ class DietRecordDetailPage extends ConsumerWidget {
@override Widget build(BuildContext context, WidgetRef ref) { @override Widget build(BuildContext context, WidgetRef ref) {
final service = ref.watch(dietServiceProvider); final service = ref.watch(dietServiceProvider);
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFF8F9FC), backgroundColor: AppTheme.bg,
appBar: AppBar(title: const Text('饮食详情')), appBar: AppBar(title: const Text('饮食详情')),
body: FutureBuilder<List<Map<String, dynamic>>>( body: FutureBuilder<List<Map<String, dynamic>>>(
future: service.getRecords(), future: service.getRecords(),
builder: (ctx, snap) { builder: (ctx, snap) {
if (!snap.hasData) return const Center(child: CircularProgressIndicator(color: Color(0xFF6C5CE7))); if (!snap.hasData) return const Center(child: CircularProgressIndicator(color: AppTheme.primary));
final d = snap.data!.firstWhere((r) => r['id']?.toString() == id, orElse: () => <String, dynamic>{}); final d = snap.data!.firstWhere((r) => r['id']?.toString() == id, orElse: () => <String, dynamic>{});
if (d.isEmpty) return const Center(child: Text('记录不存在')); if (d.isEmpty) return const Center(child: Text('记录不存在'));
final items = (d['foodItems'] as List?)?.cast<Map<String, dynamic>>() ?? []; final items = (d['foodItems'] as List?)?.cast<Map<String, dynamic>>() ?? [];
@@ -89,11 +91,11 @@ class DietRecordDetailPage extends ConsumerWidget {
final mealNames = {'Breakfast':'早餐','Lunch':'午餐','Dinner':'晚餐','Snack':'加餐'}; final mealNames = {'Breakfast':'早餐','Lunch':'午餐','Dinner':'晚餐','Snack':'加餐'};
return ListView(padding: const EdgeInsets.all(16), children: [ return ListView(padding: const EdgeInsets.all(16), children: [
Container(padding: const EdgeInsets.all(16), decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16)), child: Row(children: [ Container(padding: const EdgeInsets.all(16), decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16)), child: Row(children: [
Container(width: 48, height: 48, decoration: BoxDecoration(color: const Color(0xFFEDEAFF), borderRadius: BorderRadius.circular(14)), child: const Icon(Icons.restaurant, size: 24, color: Color(0xFF6C5CE7))), Container(width: 48, height: 48, decoration: BoxDecoration(color: AppTheme.primaryLight, borderRadius: BorderRadius.circular(14)), child: const Icon(Icons.restaurant, size: 28, color: AppTheme.primary)),
const SizedBox(width: 14), const SizedBox(width: 14),
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(mealNames[d['mealType']?.toString()] ?? '', style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w700)), Text(mealNames[d['mealType']?.toString()] ?? '', style: const TextStyle(fontSize: 21, fontWeight: FontWeight.w700)),
Text('$totalCal 千卡 · ${d['recordedAt'] ?? ''}', style: const TextStyle(fontSize: 13, color: Color(0xFF999999))), Text('$totalCal 千卡 · ${d['recordedAt'] ?? ''}', style: const TextStyle(fontSize: 16, color: Color(0xFF999999))),
]), ]),
])), ])),
const SizedBox(height: 16), const SizedBox(height: 16),
@@ -103,10 +105,10 @@ class DietRecordDetailPage extends ConsumerWidget {
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(14)), decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(14)),
child: Row(children: [ child: Row(children: [
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(f['name']?.toString() ?? '', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), Text(f['name']?.toString() ?? '', style: const TextStyle(fontSize: 19, fontWeight: FontWeight.w600)),
if ((f['portion']?.toString() ?? '').isNotEmpty) Text(f['portion']!.toString(), style: const TextStyle(fontSize: 13, color: Color(0xFF888888))), if ((f['portion']?.toString() ?? '').isNotEmpty) Text(f['portion']!.toString(), style: const TextStyle(fontSize: 16, color: Color(0xFF888888))),
])), ])),
Text('${f['calories'] ?? 0} kcal', style: const TextStyle(fontSize: 15, color: Color(0xFF6C5CE7), fontWeight: FontWeight.w600)), Text('${f['calories'] ?? 0} kcal', style: const TextStyle(fontSize: 18, color: AppTheme.primary, fontWeight: FontWeight.w600)),
]), ]),
)), )),
]); ]);
@@ -139,9 +141,9 @@ class _ExercisePlanPageState extends ConsumerState<ExercisePlanPage> {
@override Widget build(BuildContext context) { @override Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFF8F9FC), backgroundColor: AppTheme.bg,
appBar: AppBar(title: const Text('运动计划')), appBar: AppBar(title: const Text('运动计划')),
floatingActionButton: FloatingActionButton(onPressed: () { pushRoute(ref, 'exerciseCreate'); }, backgroundColor: const Color(0xFF6C5CE7), child: const Icon(Icons.add)), floatingActionButton: FloatingActionButton(onPressed: () { pushRoute(ref, 'exerciseCreate'); }, backgroundColor: AppTheme.primary, child: const Icon(Icons.add)),
body: FutureBuilder<List<Map<String, dynamic>>>( body: FutureBuilder<List<Map<String, dynamic>>>(
future: _future, future: _future,
builder: (ctx, snap) { builder: (ctx, snap) {
@@ -179,14 +181,14 @@ class _ExercisePlanPageState extends ConsumerState<ExercisePlanPage> {
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16)), decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16)),
child: Row(children: [ child: Row(children: [
Container(width: 44, height: 44, decoration: BoxDecoration(color: const Color(0xFFEDEAFF), borderRadius: BorderRadius.circular(12)), child: const Icon(Icons.directions_run, size: 22, color: Color(0xFF6C5CE7))), Container(width: 44, height: 44, decoration: BoxDecoration(color: AppTheme.primaryLight, borderRadius: BorderRadius.circular(12)), child: const Icon(Icons.directions_run, size: 25, color: AppTheme.primary)),
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(exerciseName, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), Text(exerciseName, style: const TextStyle(fontSize: 19, fontWeight: FontWeight.w600)),
const SizedBox(height: 2), const SizedBox(height: 2),
Text('$startDate 起 · ${items.first['durationMinutes'] ?? 0}分钟/天', style: const TextStyle(fontSize: 13, color: Color(0xFF888888))), Text('$startDate 起 · ${items.first['durationMinutes'] ?? 0}分钟/天', style: const TextStyle(fontSize: 16, color: Color(0xFF888888))),
const SizedBox(height: 6), const SizedBox(height: 6),
Text('$done/$total 天已完成', style: const TextStyle(fontSize: 12, color: Color(0xFF6C5CE7))), Text('$done/$total 天已完成', style: const TextStyle(fontSize: 15, color: AppTheme.primary)),
])), ])),
GestureDetector( GestureDetector(
onTap: todayItem != null ? () => _checkIn(p['id']?.toString() ?? '', todayItem['id']?.toString() ?? '') : null, onTap: todayItem != null ? () => _checkIn(p['id']?.toString() ?? '', todayItem['id']?.toString() ?? '') : null,
@@ -194,10 +196,10 @@ class _ExercisePlanPageState extends ConsumerState<ExercisePlanPage> {
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
width: 40, height: 40, width: 40, height: 40,
decoration: BoxDecoration( decoration: BoxDecoration(
color: todayDone ? const Color(0xFFDCFCE7) : (todayItem != null ? const Color(0xFFEDEAFF) : const Color(0xFFF5F5F5)), color: todayDone ? const Color(0xFFDCFCE7) : (todayItem != null ? AppTheme.primaryLight : const Color(0xFFF5F5F5)),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
child: Icon(todayDone ? Icons.check_circle : Icons.check_circle_outline, size: 24, color: todayDone ? const Color(0xFF43A047) : const Color(0xFFBBBBBB)), child: Icon(todayDone ? Icons.check_circle : Icons.check_circle_outline, size: 28, color: todayDone ? AppTheme.success : const Color(0xFFBBBBBB)),
), ),
), ),
]), ]),
@@ -238,7 +240,7 @@ class _ExercisePlanCreatePageState extends ConsumerState<ExercisePlanCreatePage>
@override Widget build(BuildContext context) { @override Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFF8F9FC), backgroundColor: AppTheme.bg,
appBar: AppBar(title: const Text('新建计划')), appBar: AppBar(title: const Text('新建计划')),
body: ListView(padding: const EdgeInsets.all(16), children: [ body: ListView(padding: const EdgeInsets.all(16), children: [
_field('运动名称', _nameCtrl, hint: '如: 散步、慢跑、太极'), _field('运动名称', _nameCtrl, hint: '如: 散步、慢跑、太极'),
@@ -249,27 +251,27 @@ class _ExercisePlanCreatePageState extends ConsumerState<ExercisePlanCreatePage>
const SizedBox(height: 16), const SizedBox(height: 16),
_dateField('结束日期', _end, (d) => setState(() => _end = d)), _dateField('结束日期', _end, (d) => setState(() => _end = d)),
const SizedBox(height: 32), const SizedBox(height: 32),
SizedBox(width: double.infinity, child: ElevatedButton(onPressed: _save, style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFF6C5CE7), foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), padding: const EdgeInsets.symmetric(vertical: 14)), child: const Text('保存计划', style: TextStyle(fontSize: 16)))), SizedBox(width: double.infinity, child: ElevatedButton(onPressed: _save, style: ElevatedButton.styleFrom(backgroundColor: AppTheme.primary, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), padding: const EdgeInsets.symmetric(vertical: 14)), child: const Text('保存计划', style: TextStyle(fontSize: 19)))),
]), ]),
); );
} }
Widget _field(String label, TextEditingController ctrl, {String? hint, bool number = false}) { Widget _field(String label, TextEditingController ctrl, {String? hint, bool number = false}) {
return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(label, style: const TextStyle(fontSize: 14, color: Color(0xFF666666))), Text(label, style: const TextStyle(fontSize: 17, color: Color(0xFF666666))),
const SizedBox(height: 6), const SizedBox(height: 6),
TextField(controller: ctrl, keyboardType: number ? TextInputType.number : null, decoration: InputDecoration(hintText: hint, filled: true, fillColor: Colors.white, border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none)), style: const TextStyle(fontSize: 16)), TextField(controller: ctrl, keyboardType: number ? TextInputType.number : null, decoration: InputDecoration(hintText: hint, filled: true, fillColor: Colors.white, border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none)), style: const TextStyle(fontSize: 19)),
]); ]);
} }
Widget _dateField(String label, DateTime val, ValueChanged<DateTime> onChanged) { Widget _dateField(String label, DateTime val, ValueChanged<DateTime> onChanged) {
return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(label, style: const TextStyle(fontSize: 14, color: Color(0xFF666666))), Text(label, style: const TextStyle(fontSize: 17, color: Color(0xFF666666))),
const SizedBox(height: 6), const SizedBox(height: 6),
GestureDetector( GestureDetector(
onTap: () async { final d = await showDatePicker(context: context, initialDate: val, firstDate: DateTime(2024), lastDate: DateTime(2030)); if (d != null) onChanged(d); }, onTap: () async { final d = await showDatePicker(context: context, initialDate: val, firstDate: DateTime(2024), lastDate: DateTime(2030)); if (d != null) onChanged(d); },
child: Container(width: double.infinity, padding: const EdgeInsets.all(14), decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12)), child: Container(width: double.infinity, padding: const EdgeInsets.all(14), decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12)),
child: Text('${val.year}-${val.month.toString().padLeft(2,'0')}-${val.day.toString().padLeft(2,'0')}', style: const TextStyle(fontSize: 16))), child: Text('${val.year}-${val.month.toString().padLeft(2,'0')}-${val.day.toString().padLeft(2,'0')}', style: const TextStyle(fontSize: 19))),
), ),
]); ]);
} }
@@ -295,7 +297,7 @@ class _FollowUpListPageState extends ConsumerState<FollowUpListPage> {
future: _future, future: _future,
builder: (ctx, snap) { builder: (ctx, snap) {
if (snap.connectionState == ConnectionState.waiting) { if (snap.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator(color: Color(0xFF8B9CF7))); return const Center(child: CircularProgressIndicator(color: AppTheme.primaryLight));
} }
final list = snap.data ?? []; final list = snap.data ?? [];
if (list.isEmpty) { if (list.isEmpty) {
@@ -305,7 +307,7 @@ class _FollowUpListPageState extends ConsumerState<FollowUpListPage> {
const SizedBox(height: 12), const SizedBox(height: 12),
Text('暂无随访安排', style: Theme.of(context).textTheme.bodyMedium), Text('暂无随访安排', style: Theme.of(context).textTheme.bodyMedium),
const SizedBox(height: 4), const SizedBox(height: 4),
const Text('医生创建随访后将显示在这里', style: TextStyle(fontSize: 13, color: Color(0xFF999999))), const Text('医生创建随访后将显示在这里', style: TextStyle(fontSize: 16, color: Color(0xFF999999))),
]), ]),
); );
} }
@@ -335,8 +337,8 @@ class _FollowUpItem extends StatelessWidget {
Color _statusColor(String? status) { Color _statusColor(String? status) {
switch (status) { switch (status) {
case 'Upcoming': return const Color(0xFF4F6EF7); case 'Upcoming': return const Color(0xFF4F6EF7);
case 'Completed': return const Color(0xFF43A047); case 'Completed': return AppTheme.success;
case 'Cancelled': return const Color(0xFFE53935); case 'Cancelled': return AppTheme.error;
default: return const Color(0xFF999999); default: return const Color(0xFF999999);
} }
} }
@@ -363,27 +365,27 @@ class _FollowUpItem extends StatelessWidget {
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
boxShadow: [BoxShadow(color: const Color(0xFF8B9CF7).withAlpha(10), blurRadius: 4, offset: const Offset(0, 2))], boxShadow: [BoxShadow(color: AppTheme.primary.withAlpha(10), blurRadius: 4, offset: const Offset(0, 2))],
), ),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Row(children: [ Row(children: [
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(color: bg, borderRadius: BorderRadius.circular(8)), decoration: BoxDecoration(color: bg, borderRadius: BorderRadius.circular(8)),
child: Text(label, style: TextStyle(fontSize: 12, color: color, fontWeight: FontWeight.w500)), child: Text(label, style: TextStyle(fontSize: 15, color: color, fontWeight: FontWeight.w500)),
), ),
const Spacer(), const Spacer(),
if (item['doctorName'] != null) if (item['doctorName'] != null)
Text('👨‍⚕️ ${item['doctorName']}', style: const TextStyle(fontSize: 12, color: Color(0xFF999999))), Text('👨‍⚕️ ${item['doctorName']}', style: const TextStyle(fontSize: 15, color: Color(0xFF999999))),
]), ]),
const SizedBox(height: 12), const SizedBox(height: 12),
Text(item['title']?.toString() ?? '', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), Text(item['title']?.toString() ?? '', style: const TextStyle(fontSize: 19, fontWeight: FontWeight.w600)),
const SizedBox(height: 4), const SizedBox(height: 4),
if (item['scheduledAt'] != null) if (item['scheduledAt'] != null)
Text(_formatDateTime(item['scheduledAt']?.toString()), style: TextStyle(fontSize: 14, color: Colors.grey[500])), Text(_formatDateTime(item['scheduledAt']?.toString()), style: TextStyle(fontSize: 17, color: Colors.grey[500])),
if ((item['notes']?.toString() ?? '').isNotEmpty) ...[ if ((item['notes']?.toString() ?? '').isNotEmpty) ...[
const SizedBox(height: 8), const SizedBox(height: 8),
Text(item['notes']?.toString() ?? '', style: TextStyle(fontSize: 13, color: Colors.grey[600])), Text(item['notes']?.toString() ?? '', style: TextStyle(fontSize: 16, color: Colors.grey[600])),
], ],
]), ]),
); );
@@ -467,11 +469,11 @@ class _HealthArchivePageState extends ConsumerState<HealthArchivePage> {
} }
@override Widget build(BuildContext context) { @override Widget build(BuildContext context) {
if (_loading) return Scaffold(appBar: AppBar(title: const Text('健康档案')), body: const Center(child: CircularProgressIndicator(color: Color(0xFF6C5CE7)))); if (_loading) return Scaffold(appBar: AppBar(title: const Text('健康档案')), body: const Center(child: CircularProgressIndicator(color: AppTheme.primary)));
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFF8F9FC), backgroundColor: AppTheme.bg,
appBar: AppBar(title: const Text('健康档案'), actions: [ appBar: AppBar(title: const Text('健康档案'), actions: [
TextButton(onPressed: _save, child: const Text('保存', style: TextStyle(color: Color(0xFF6C5CE7), fontWeight: FontWeight.w600))), TextButton(onPressed: _save, child: const Text('保存', style: TextStyle(color: AppTheme.primary, fontWeight: FontWeight.w600))),
]), ]),
body: ListView(padding: const EdgeInsets.all(16), children: [ body: ListView(padding: const EdgeInsets.all(16), children: [
_sectionTitle('个人资料'), _sectionTitle('个人资料'),
@@ -500,21 +502,21 @@ class _HealthArchivePageState extends ConsumerState<HealthArchivePage> {
Widget _sectionTitle(String title) => Padding( Widget _sectionTitle(String title) => Padding(
padding: const EdgeInsets.only(left: 4, top: 16, bottom: 8), padding: const EdgeInsets.only(left: 4, top: 16, bottom: 8),
child: Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: Color(0xFF6C5CE7))), child: Text(title, style: const TextStyle(fontSize: 19, fontWeight: FontWeight.w700, color: AppTheme.primary)),
); );
Widget _editableCard({required List<Widget> children}) => Container( Widget _editableCard({required List<Widget> children}) => Container(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [BoxShadow(color: const Color(0xFF6C5CE7).withAlpha(8), blurRadius: 8, offset: const Offset(0, 2))]), decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [BoxShadow(color: AppTheme.primary.withAlpha(8), blurRadius: 8, offset: const Offset(0, 2))]),
child: Column(children: children), child: Column(children: children),
); );
Widget _field(String label, TextEditingController ctrl, {String? hint}) => Padding( Widget _field(String label, TextEditingController ctrl, {String? hint}) => Padding(
padding: const EdgeInsets.only(bottom: 12), padding: const EdgeInsets.only(bottom: 12),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(label, style: const TextStyle(fontSize: 13, color: Color(0xFF888888))), Text(label, style: const TextStyle(fontSize: 16, color: Color(0xFF888888))),
const SizedBox(height: 4), const SizedBox(height: 4),
TextField(controller: ctrl, decoration: InputDecoration(hintText: hint, filled: true, fillColor: const Color(0xFFF4F5FA), contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), border: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: BorderSide.none)), style: const TextStyle(fontSize: 15)), TextField(controller: ctrl, decoration: InputDecoration(hintText: hint, filled: true, fillColor: const Color(0xFFF4F5FA), contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), border: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: BorderSide.none)), style: const TextStyle(fontSize: 18)),
]), ]),
); );
} }
@@ -581,7 +583,7 @@ class _HealthCalendarPageState extends ConsumerState<HealthCalendarPage> {
), ),
Text( Text(
'${_currentMonth.year}${_currentMonth.month}', '${_currentMonth.year}${_currentMonth.month}',
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600), style: const TextStyle(fontSize: 21, fontWeight: FontWeight.w600),
), ),
IconButton( IconButton(
icon: const Icon(Icons.chevron_right, size: 32), icon: const Icon(Icons.chevron_right, size: 32),
@@ -594,7 +596,7 @@ class _HealthCalendarPageState extends ConsumerState<HealthCalendarPage> {
Widget _buildWeekdayHeader() { Widget _buildWeekdayHeader() {
const weekdays = ['', '', '', '', '', '', '']; const weekdays = ['', '', '', '', '', '', ''];
return Row(children: weekdays.map((day) => Expanded( return Row(children: weekdays.map((day) => Expanded(
child: Center(child: Text(day, style: TextStyle(fontSize: 14, color: Colors.grey[500]))), child: Center(child: Text(day, style: TextStyle(fontSize: 17, color: Colors.grey[500]))),
)).toList()); )).toList());
} }
@@ -631,7 +633,7 @@ class _HealthCalendarPageState extends ConsumerState<HealthCalendarPage> {
return Container( return Container(
decoration: isToday ? BoxDecoration( decoration: isToday ? BoxDecoration(
color: const Color(0xFF8B9CF7), color: AppTheme.primary,
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
) : null, ) : null,
child: Stack( child: Stack(
@@ -640,7 +642,7 @@ class _HealthCalendarPageState extends ConsumerState<HealthCalendarPage> {
Text( Text(
'$day', '$day',
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 19,
color: isToday ? Colors.white : Colors.black, color: isToday ? Colors.white : Colors.black,
fontWeight: isToday ? FontWeight.w600 : FontWeight.normal, fontWeight: isToday ? FontWeight.w600 : FontWeight.normal,
), ),
@@ -670,8 +672,8 @@ class _HealthCalendarPageState extends ConsumerState<HealthCalendarPage> {
Color _getEventColor(String type) { Color _getEventColor(String type) {
switch (type) { switch (type) {
case 'medication': return const Color(0xFF8B9CF7); case 'medication': return AppTheme.primary;
case 'exercise': return const Color(0xFF43A047); case 'exercise': return AppTheme.success;
case 'followup': return const Color(0xFFF59E0B); case 'followup': return const Color(0xFFF59E0B);
default: return Colors.grey; default: return Colors.grey;
} }
@@ -679,8 +681,8 @@ class _HealthCalendarPageState extends ConsumerState<HealthCalendarPage> {
Widget _buildLegend() { Widget _buildLegend() {
final items = [ final items = [
{'color': const Color(0xFF8B9CF7), 'label': '用药提醒'}, {'color': AppTheme.primary, 'label': '用药提醒'},
{'color': const Color(0xFF43A047), 'label': '运动计划'}, {'color': AppTheme.success, 'label': '运动计划'},
{'color': const Color(0xFFF59E0B), 'label': '复查随访'}, {'color': const Color(0xFFF59E0B), 'label': '复查随访'},
]; ];
return Container( return Container(
@@ -688,7 +690,7 @@ class _HealthCalendarPageState extends ConsumerState<HealthCalendarPage> {
child: Row(mainAxisAlignment: MainAxisAlignment.center, children: items.map((item) => Row(children: [ child: Row(mainAxisAlignment: MainAxisAlignment.center, children: items.map((item) => Row(children: [
Container(width: 10, height: 10, decoration: BoxDecoration(color: item['color'] as Color, borderRadius: BorderRadius.circular(5))), Container(width: 10, height: 10, decoration: BoxDecoration(color: item['color'] as Color, borderRadius: BorderRadius.circular(5))),
const SizedBox(width: 4), const SizedBox(width: 4),
Text(item['label'] as String, style: TextStyle(fontSize: 12, color: Colors.grey[600])), Text(item['label'] as String, style: TextStyle(fontSize: 15, color: Colors.grey[600])),
const SizedBox(width: 20), const SizedBox(width: 20),
])).toList()), ])).toList()),
); );
@@ -782,7 +784,7 @@ class StaticTextPage extends ConsumerWidget {
), ),
body: SingleChildScrollView( body: SingleChildScrollView(
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
child: Text(contents[type] ?? '内容加载中...', style: const TextStyle(fontSize: 14, height: 1.8, color: Color(0xFF333333))), child: Text(contents[type] ?? '内容加载中...', style: const TextStyle(fontSize: 17, height: 1.8, color: Color(0xFF333333))),
), ),
); );
} }
@@ -831,10 +833,10 @@ class _SwipeDeleteTileState extends State<_SwipeDeleteTile> with SingleTickerPro
return Stack(children: [ return Stack(children: [
Positioned.fill(child: Container( Positioned.fill(child: Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
decoration: BoxDecoration(color: const Color(0xFFE53935), borderRadius: BorderRadius.circular(16)), decoration: BoxDecoration(color: AppTheme.error, borderRadius: BorderRadius.circular(16)),
child: const Align( child: const Align(
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: Padding(padding: EdgeInsets.only(right: 20), child: Icon(Icons.delete_outline, color: Colors.white, size: 24)), child: Padding(padding: EdgeInsets.only(right: 20), child: Icon(Icons.delete_outline, color: Colors.white, size: 28)),
), ),
)), )),
GestureDetector( GestureDetector(

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../core/app_theme.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/navigation_provider.dart'; import '../../core/navigation_provider.dart';
import 'report_pages.dart'; import 'report_pages.dart';
@@ -31,12 +32,12 @@ class _AiAnalysisPageState extends ConsumerState<AiAnalysisPage> {
if (analysis == null) { if (analysis == null) {
return Scaffold( return Scaffold(
appBar: AppBar(title: const Text('AI 解读')), appBar: AppBar(title: const Text('AI 解读')),
body: const Center(child: CircularProgressIndicator(color: Color(0xFF8B9CF7))), body: const Center(child: CircularProgressIndicator(color: AppTheme.primary)),
); );
} }
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFF8F9FC), backgroundColor: AppTheme.bg,
appBar: AppBar( appBar: AppBar(
title: const Text('AI 智能解读'), title: const Text('AI 智能解读'),
leading: IconButton( leading: IconButton(
@@ -74,15 +75,15 @@ class _AiAnalysisPageState extends ConsumerState<AiAnalysisPage> {
return Container( return Container(
width: double.infinity, padding: const EdgeInsets.all(16), width: double.infinity, padding: const EdgeInsets.all(16),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16), decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16),
boxShadow: [BoxShadow(color: const Color(0xFF8B9CF7).withAlpha(10), blurRadius: 8)]), boxShadow: [BoxShadow(color: AppTheme.primary.withAlpha(10), blurRadius: 8)]),
child: Row(children: [ child: Row(children: [
Container(width: 48, height: 48, decoration: BoxDecoration(color: const Color(0xFFF0F2FF), borderRadius: BorderRadius.circular(12)), Container(width: 48, height: 48, decoration: BoxDecoration(color: AppTheme.primaryLight, borderRadius: BorderRadius.circular(12)),
child: const Icon(Icons.auto_awesome, size: 24, color: Color(0xFF8B9CF7))), child: const Icon(Icons.auto_awesome, size: 28, color: AppTheme.primary)),
const SizedBox(width: 14), const SizedBox(width: 14),
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(analysis.reportType, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w700)), Text(analysis.reportType, style: const TextStyle(fontSize: 21, fontWeight: FontWeight.w700)),
const SizedBox(height: 4), const SizedBox(height: 4),
const Text('AI 预解读结果 · 待医生确认', style: TextStyle(fontSize: 13, color: Color(0xFF999999))), const Text('AI 预解读结果 · 待医生确认', style: TextStyle(fontSize: 16, color: Color(0xFF999999))),
])), ])),
]), ]),
); );
@@ -97,12 +98,12 @@ class _AiAnalysisPageState extends ConsumerState<AiAnalysisPage> {
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
), ),
child: Row(mainAxisSize: MainAxisSize.min, children: [ child: Row(mainAxisSize: MainAxisSize.min, children: [
Icon(isReviewed ? Icons.check_circle : Icons.hourglass_empty, size: 18, Icon(isReviewed ? Icons.check_circle : Icons.hourglass_empty, size: 21,
color: isReviewed ? const Color(0xFF43A047) : const Color(0xFFF59E0B)), color: isReviewed ? AppTheme.success : const Color(0xFFF59E0B)),
const SizedBox(width: 8), const SizedBox(width: 8),
Text(isReviewed ? '已通过医生审核' : '等待医生审核中', Text(isReviewed ? '已通过医生审核' : '等待医生审核中',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, style: TextStyle(fontSize: 17, fontWeight: FontWeight.w500,
color: isReviewed ? const Color(0xFF43A047) : const Color(0xFFF59E0B))), color: isReviewed ? AppTheme.success : const Color(0xFFF59E0B))),
]), ]),
); );
} }
@@ -114,7 +115,7 @@ class _AiAnalysisPageState extends ConsumerState<AiAnalysisPage> {
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
const Padding( const Padding(
padding: EdgeInsets.fromLTRB(16, 16, 16, 8), padding: EdgeInsets.fromLTRB(16, 16, 16, 8),
child: Text('指标分析', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w700)), child: Text('指标分析', style: TextStyle(fontSize: 19, fontWeight: FontWeight.w700)),
), ),
...analysis.indicators.map((ind) => _buildIndicatorRow(ind)), ...analysis.indicators.map((ind) => _buildIndicatorRow(ind)),
const SizedBox(height: 8), const SizedBox(height: 8),
@@ -126,9 +127,9 @@ class _AiAnalysisPageState extends ConsumerState<AiAnalysisPage> {
final Color color; final Color color;
final IconData icon; final IconData icon;
switch (ind.status) { switch (ind.status) {
case 'high': color = const Color(0xFFE53935); icon = Icons.arrow_upward; break; case 'high': color = AppTheme.error; icon = Icons.arrow_upward; break;
case 'low': color = const Color(0xFFF9A825); icon = Icons.arrow_downward; break; case 'low': color = const Color(0xFFF9A825); icon = Icons.arrow_downward; break;
default: color = const Color(0xFF43A047); icon = Icons.check_circle; default: color = AppTheme.success; icon = Icons.check_circle;
} }
return Container( return Container(
margin: const EdgeInsets.symmetric(horizontal: 16), margin: const EdgeInsets.symmetric(horizontal: 16),
@@ -137,15 +138,15 @@ class _AiAnalysisPageState extends ConsumerState<AiAnalysisPage> {
child: Row(children: [ child: Row(children: [
Expanded( Expanded(
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(ind.name, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500)), Text(ind.name, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w500)),
if (ind.referenceRange != null && ind.referenceRange!.isNotEmpty) if (ind.referenceRange != null && ind.referenceRange!.isNotEmpty)
Text('参考值: ${ind.referenceRange}', style: const TextStyle(fontSize: 12, color: Color(0xFF999999))), Text('参考值: ${ind.referenceRange}', style: const TextStyle(fontSize: 15, color: Color(0xFF999999))),
]), ]),
), ),
Column(crossAxisAlignment: CrossAxisAlignment.end, children: [ Column(crossAxisAlignment: CrossAxisAlignment.end, children: [
Text('${ind.value} ${ind.unit}', Text('${ind.value} ${ind.unit}',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: color)), style: TextStyle(fontSize: 19, fontWeight: FontWeight.w600, color: color)),
Icon(icon, size: 14, color: color), Icon(icon, size: 17, color: color),
]), ]),
]), ]),
); );
@@ -158,19 +159,19 @@ class _AiAnalysisPageState extends ConsumerState<AiAnalysisPage> {
border: Border.all(color: const Color(0xFFFDE68A))), border: Border.all(color: const Color(0xFFFDE68A))),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
const Row(children: [ const Row(children: [
Text('💡', style: TextStyle(fontSize: 18)), Text('💡', style: TextStyle(fontSize: 21)),
SizedBox(width: 8), SizedBox(width: 8),
Text('综合解读', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: Color(0xFFD97706))), Text('综合解读', style: TextStyle(fontSize: 19, fontWeight: FontWeight.w700, color: Color(0xFFD97706))),
]), ]),
const SizedBox(height: 10), const SizedBox(height: 10),
Text(analysis.summary.isNotEmpty ? analysis.summary : '暂无 AI 解读结果', Text(analysis.summary.isNotEmpty ? analysis.summary : '暂无 AI 解读结果',
style: const TextStyle(fontSize: 14, color: Color(0xFF92400E), height: 1.6)), style: const TextStyle(fontSize: 17, color: Color(0xFF92400E), height: 1.6)),
const SizedBox(height: 10), const SizedBox(height: 10),
Container( Container(
width: double.infinity, padding: const EdgeInsets.all(10), width: double.infinity, padding: const EdgeInsets.all(10),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(10)), decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(10)),
child: const Text('⚠️ AI 解读仅供参考,请以医生诊断为准', child: const Text('⚠️ AI 解读仅供参考,请以医生诊断为准',
style: TextStyle(fontSize: 12, color: Color(0xFFD97706))), style: TextStyle(fontSize: 15, color: Color(0xFFD97706))),
), ),
]), ]),
); );
@@ -183,18 +184,18 @@ class _AiAnalysisPageState extends ConsumerState<AiAnalysisPage> {
border: Border.all(color: const Color(0xFF86EFAC))), border: Border.all(color: const Color(0xFF86EFAC))),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
const Row(children: [ const Row(children: [
Text('', style: TextStyle(fontSize: 18)), Text('', style: TextStyle(fontSize: 21)),
SizedBox(width: 8), SizedBox(width: 8),
Text('医生审核意见', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: Color(0xFF2E7D32))), Text('医生审核意见', style: TextStyle(fontSize: 19, fontWeight: FontWeight.w700, color: Color(0xFF2E7D32))),
]), ]),
if (report.doctorName != null) ...[ if (report.doctorName != null) ...[
const SizedBox(height: 4), const SizedBox(height: 4),
Text('审核医生:${report.doctorName}', style: const TextStyle(fontSize: 13, color: Color(0xFF4CAF50))), Text('审核医生:${report.doctorName}', style: const TextStyle(fontSize: 16, color: Color(0xFF4CAF50))),
], ],
if (report.reviewedAt != null) ...[ if (report.reviewedAt != null) ...[
const SizedBox(height: 2), const SizedBox(height: 2),
Text('审核时间:${report.reviewedAt!.toLocal().toString().substring(0, 16)}', Text('审核时间:${report.reviewedAt!.toLocal().toString().substring(0, 16)}',
style: const TextStyle(fontSize: 12, color: Color(0xFF81C784))), style: const TextStyle(fontSize: 15, color: Color(0xFF81C784))),
], ],
if (report.severity != null) ...[ if (report.severity != null) ...[
const SizedBox(height: 8), const SizedBox(height: 8),
@@ -206,7 +207,7 @@ class _AiAnalysisPageState extends ConsumerState<AiAnalysisPage> {
width: double.infinity, padding: const EdgeInsets.all(12), width: double.infinity, padding: const EdgeInsets.all(12),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(10)), decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(10)),
child: Text('💬 ${report.doctorComment!}', child: Text('💬 ${report.doctorComment!}',
style: const TextStyle(fontSize: 15, color: Color(0xFF1A1A1A), height: 1.5)), style: const TextStyle(fontSize: 18, color: Color(0xFF1A1A1A), height: 1.5)),
), ),
], ],
if (report.doctorRecommendation != null && report.doctorRecommendation!.isNotEmpty) ...[ if (report.doctorRecommendation != null && report.doctorRecommendation!.isNotEmpty) ...[
@@ -215,7 +216,7 @@ class _AiAnalysisPageState extends ConsumerState<AiAnalysisPage> {
width: double.infinity, padding: const EdgeInsets.all(12), width: double.infinity, padding: const EdgeInsets.all(12),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(10)), decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(10)),
child: Text('📝 ${report.doctorRecommendation!}', child: Text('📝 ${report.doctorRecommendation!}',
style: const TextStyle(fontSize: 14, color: Color(0xFF333333), height: 1.5)), style: const TextStyle(fontSize: 17, color: Color(0xFF333333), height: 1.5)),
), ),
], ],
]), ]),
@@ -224,7 +225,7 @@ class _AiAnalysisPageState extends ConsumerState<AiAnalysisPage> {
Widget _severityBadge(String severity) { Widget _severityBadge(String severity) {
final (label, color, bg) = switch (severity) { final (label, color, bg) = switch (severity) {
'Normal' => ('🟢 正常', const Color(0xFF43A047), const Color(0xFFDCFCE7)), 'Normal' => ('🟢 正常', AppTheme.success, const Color(0xFFDCFCE7)),
'Abnormal' => ('🟡 轻度异常', const Color(0xFFF59E0B), const Color(0xFFFFF3E0)), 'Abnormal' => ('🟡 轻度异常', const Color(0xFFF59E0B), const Color(0xFFFFF3E0)),
'Severe' => ('🟠 中度异常', const Color(0xFFFF7043), const Color(0xFFFEE2E2)), 'Severe' => ('🟠 中度异常', const Color(0xFFFF7043), const Color(0xFFFEE2E2)),
'Critical' => ('🔴 重度异常', const Color(0xFFDC2626), const Color(0xFFFEE2E2)), 'Critical' => ('🔴 重度异常', const Color(0xFFDC2626), const Color(0xFFFEE2E2)),
@@ -233,7 +234,7 @@ class _AiAnalysisPageState extends ConsumerState<AiAnalysisPage> {
return Container( return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(color: bg, borderRadius: BorderRadius.circular(6)), decoration: BoxDecoration(color: bg, borderRadius: BorderRadius.circular(6)),
child: Text(label, style: TextStyle(fontSize: 12, fontWeight: FontWeight.w500, color: color)), child: Text(label, style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: color)),
); );
} }
} }

View File

@@ -5,6 +5,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import '../../core/app_theme.dart';
import '../../core/navigation_provider.dart'; import '../../core/navigation_provider.dart';
import '../../providers/auth_provider.dart'; import '../../providers/auth_provider.dart';
@@ -222,7 +224,7 @@ class ReportListPage extends ConsumerWidget {
appBar: AppBar(title: const Text('看报告')), appBar: AppBar(title: const Text('看报告')),
body: const Center( body: const Center(
child: Column(mainAxisSize: MainAxisSize.min, children: [ child: Column(mainAxisSize: MainAxisSize.min, children: [
CircularProgressIndicator(color: Color(0xFF8B9CF7)), CircularProgressIndicator(color: AppTheme.primaryLight),
SizedBox(height: 16), SizedBox(height: 16),
Text('AI 正在分析报告...'), Text('AI 正在分析报告...'),
]), ]),
@@ -252,7 +254,7 @@ class ReportListPage extends ConsumerWidget {
Widget _buildUploadButton(BuildContext context, WidgetRef ref) { Widget _buildUploadButton(BuildContext context, WidgetRef ref) {
return FloatingActionButton( return FloatingActionButton(
onPressed: () => _showUploadOptions(context, ref), onPressed: () => _showUploadOptions(context, ref),
backgroundColor: const Color(0xFF8B9CF7), backgroundColor: AppTheme.primary,
child: const Icon(Icons.add), child: const Icon(Icons.add),
); );
} }
@@ -312,12 +314,12 @@ class ReportListPage extends ConsumerWidget {
color: const Color(0xFFF0F2FF), color: const Color(0xFFF0F2FF),
borderRadius: BorderRadius.circular(60), borderRadius: BorderRadius.circular(60),
), ),
child: const Icon(Icons.file_open, size: 48, color: Color(0xFF8B9CF7)), child: const Icon(Icons.file_open, size: 48, color: AppTheme.primaryLight),
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
const Text('暂无检查报告', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w500)), const Text('暂无检查报告', style: TextStyle(fontSize: 21, fontWeight: FontWeight.w500)),
const SizedBox(height: 8), const SizedBox(height: 8),
const Text('点击下方按钮上传报告', style: TextStyle(fontSize: 14, color: Color(0xFF999999))), const Text('点击下方按钮上传报告', style: TextStyle(fontSize: 17, color: Color(0xFF999999))),
]), ]),
); );
} }
@@ -328,7 +330,7 @@ class ReportListPage extends ConsumerWidget {
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
boxShadow: [BoxShadow(color: const Color(0xFF8B9CF7).withAlpha(10), blurRadius: 4, offset: const Offset(0, 2))], boxShadow: [BoxShadow(color: AppTheme.primary.withAlpha(10), blurRadius: 4, offset: const Offset(0, 2))],
), ),
child: ListTile( child: ListTile(
leading: Container( leading: Container(
@@ -340,10 +342,10 @@ class ReportListPage extends ConsumerWidget {
), ),
child: _getReportIcon(report.type), child: _getReportIcon(report.type),
), ),
title: Text(report.title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), title: Text(report.title, style: const TextStyle(fontSize: 19, fontWeight: FontWeight.w600)),
subtitle: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ subtitle: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(report.type, style: TextStyle(fontSize: 14, color: Colors.grey[500])), Text(report.type, style: TextStyle(fontSize: 17, color: Colors.grey[500])),
Text(_formatDate(report.uploadedAt), style: TextStyle(fontSize: 12, color: Colors.grey[400])), Text(_formatDate(report.uploadedAt), style: TextStyle(fontSize: 15, color: Colors.grey[400])),
]), ]),
trailing: Row( trailing: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -355,7 +357,7 @@ class ReportListPage extends ConsumerWidget {
color: const Color(0xFFDCFCE7), color: const Color(0xFFDCFCE7),
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
), ),
child: const Text('已审核', style: TextStyle(fontSize: 10, color: Color(0xFF43A047))), child: const Text('已审核', style: TextStyle(fontSize: 13, color: Color(0xFF43A047))),
) )
else if (!report.hasAnalysis) else if (!report.hasAnalysis)
Container( Container(
@@ -364,7 +366,7 @@ class ReportListPage extends ConsumerWidget {
color: const Color(0xFFE3F2FD), color: const Color(0xFFE3F2FD),
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
), ),
child: const Text('分析中', style: TextStyle(fontSize: 10, color: Color(0xFF2196F3))), child: const Text('分析中', style: TextStyle(fontSize: 13, color: Color(0xFF2196F3))),
) )
else if (report.status == 'PendingDoctor') else if (report.status == 'PendingDoctor')
Container( Container(
@@ -373,12 +375,12 @@ class ReportListPage extends ConsumerWidget {
color: const Color(0xFFFFF3E0), color: const Color(0xFFFFF3E0),
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
), ),
child: const Text('待审核', style: TextStyle(fontSize: 10, color: Color(0xFFF59E0B))), child: const Text('待审核', style: TextStyle(fontSize: 13, color: Color(0xFFF59E0B))),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
report.hasAnalysis report.hasAnalysis
? const Icon(Icons.check_circle, size: 20, color: Color(0xFF43A047)) ? const Icon(Icons.check_circle, size: 23, color: Color(0xFF43A047))
: const Icon(Icons.arrow_forward_ios, size: 18, color: Color(0xFFCCCCCC)), : const Icon(Icons.arrow_forward_ios, size: 21, color: Color(0xFFCCCCCC)),
], ],
), ),
onTap: () { onTap: () {
@@ -390,13 +392,13 @@ class ReportListPage extends ConsumerWidget {
Widget _getReportIcon(String type) { Widget _getReportIcon(String type) {
final icons = { final icons = {
'血液检查': const Icon(Icons.bloodtype, size: 24, color: Color(0xFF8B9CF7)), '血液检查': const Icon(Icons.bloodtype, size: 28, color: AppTheme.primaryLight),
'心电图': const Icon(Icons.monitor_heart, size: 24, color: Color(0xFF8B9CF7)), '心电图': const Icon(Icons.monitor_heart, size: 28, color: AppTheme.primaryLight),
'超声检查': const Icon(Icons.image, size: 24, color: Color(0xFF8B9CF7)), '超声检查': const Icon(Icons.image, size: 28, color: AppTheme.primaryLight),
'影像报告': const Icon(Icons.image, size: 24, color: Color(0xFF8B9CF7)), '影像报告': const Icon(Icons.image, size: 28, color: AppTheme.primaryLight),
'PDF文档': const Icon(Icons.picture_as_pdf, size: 24, color: Color(0xFF8B9CF7)), 'PDF文档': const Icon(Icons.picture_as_pdf, size: 28, color: AppTheme.primaryLight),
}; };
return icons[type] ?? const Icon(Icons.description, size: 24, color: Color(0xFF8B9CF7)); return icons[type] ?? const Icon(Icons.description, size: 28, color: AppTheme.primaryLight);
} }
String _formatDate(DateTime date) { String _formatDate(DateTime date) {
@@ -438,7 +440,7 @@ class _ReportDetailPageState extends ConsumerState<ReportDetailPage> {
if (analysis == null) { if (analysis == null) {
return Scaffold( return Scaffold(
appBar: AppBar(title: const Text('报告详情')), appBar: AppBar(title: const Text('报告详情')),
body: const Center(child: CircularProgressIndicator(color: Color(0xFF8B9CF7))), body: const Center(child: CircularProgressIndicator(color: AppTheme.primaryLight)),
); );
} }
@@ -470,7 +472,7 @@ class _ReportDetailPageState extends ConsumerState<ReportDetailPage> {
width: double.infinity, height: 48, width: double.infinity, height: 48,
child: ElevatedButton( child: ElevatedButton(
onPressed: () => pushRoute(ref, 'aiAnalysis', params: {'id': widget.id}), onPressed: () => pushRoute(ref, 'aiAnalysis', params: {'id': widget.id}),
style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFF8B9CF7), foregroundColor: Colors.white, style: ElevatedButton.styleFrom(backgroundColor: AppTheme.primary, foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24))), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24))),
child: const Text('查看 AI 智能解读'), child: const Text('查看 AI 智能解读'),
), ),
@@ -491,17 +493,17 @@ class _ReportDetailPageState extends ConsumerState<ReportDetailPage> {
), ),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Row(children: [ Row(children: [
const Text('', style: TextStyle(fontSize: 20)), const Text('', style: TextStyle(fontSize: 23)),
const SizedBox(width: 8), const SizedBox(width: 8),
const Text('医生审核意见', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF2E7D32))), const Text('医生审核意见', style: TextStyle(fontSize: 19, fontWeight: FontWeight.w600, color: Color(0xFF2E7D32))),
]), ]),
if (report.doctorName != null) ...[ if (report.doctorName != null) ...[
const SizedBox(height: 4), const SizedBox(height: 4),
Text('审核医生:${report.doctorName}', style: const TextStyle(fontSize: 13, color: Color(0xFF4CAF50))), Text('审核医生:${report.doctorName}', style: const TextStyle(fontSize: 16, color: Color(0xFF4CAF50))),
], ],
if (report.reviewedAt != null) ...[ if (report.reviewedAt != null) ...[
const SizedBox(height: 2), const SizedBox(height: 2),
Text('审核时间:${report.reviewedAt!.toLocal().toString().substring(0, 19)}', style: const TextStyle(fontSize: 12, color: Color(0xFF81C784))), Text('审核时间:${report.reviewedAt!.toLocal().toString().substring(0, 19)}', style: const TextStyle(fontSize: 15, color: Color(0xFF81C784))),
], ],
if (report.severity != null) ...[ if (report.severity != null) ...[
const SizedBox(height: 6), const SizedBox(height: 6),
@@ -516,7 +518,7 @@ class _ReportDetailPageState extends ConsumerState<ReportDetailPage> {
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
child: Text('💬 ${report.doctorComment!}', style: const TextStyle(fontSize: 15, color: Color(0xFF1A1A1A), height: 1.5)), child: Text('💬 ${report.doctorComment!}', style: const TextStyle(fontSize: 18, color: Color(0xFF1A1A1A), height: 1.5)),
), ),
], ],
if (report.doctorRecommendation != null && report.doctorRecommendation!.isNotEmpty) ...[ if (report.doctorRecommendation != null && report.doctorRecommendation!.isNotEmpty) ...[
@@ -528,7 +530,7 @@ class _ReportDetailPageState extends ConsumerState<ReportDetailPage> {
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
child: Text('📝 ${report.doctorRecommendation!}', style: const TextStyle(fontSize: 14, color: Color(0xFF333333), height: 1.5)), child: Text('📝 ${report.doctorRecommendation!}', style: const TextStyle(fontSize: 17, color: Color(0xFF333333), height: 1.5)),
), ),
], ],
]), ]),
@@ -546,7 +548,7 @@ class _ReportDetailPageState extends ConsumerState<ReportDetailPage> {
return Container( return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(color: bg, borderRadius: BorderRadius.circular(6)), decoration: BoxDecoration(color: bg, borderRadius: BorderRadius.circular(6)),
child: Text(label, style: TextStyle(fontSize: 12, fontWeight: FontWeight.w500, color: color)), child: Text(label, style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: color)),
); );
} }
@@ -559,12 +561,12 @@ class _ReportDetailPageState extends ConsumerState<ReportDetailPage> {
), ),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Row(children: [ Row(children: [
const Text('📋', style: TextStyle(fontSize: 24)), const Text('📋', style: TextStyle(fontSize: 28)),
const SizedBox(width: 12), const SizedBox(width: 12),
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(analysis.reportType, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600)), Text(analysis.reportType, style: const TextStyle(fontSize: 21, fontWeight: FontWeight.w600)),
const SizedBox(height: 4), const SizedBox(height: 4),
const Text('AI 预解读结果', style: TextStyle(fontSize: 14, color: Color(0xFF666666))), const Text('AI 预解读结果', style: TextStyle(fontSize: 17, color: Color(0xFF666666))),
]), ]),
]), ]),
]), ]),
@@ -582,9 +584,9 @@ class _ReportDetailPageState extends ConsumerState<ReportDetailPage> {
Padding( Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Row(children: [ child: Row(children: [
const Text('🧪', style: TextStyle(fontSize: 20)), const Text('🧪', style: TextStyle(fontSize: 23)),
const SizedBox(width: 8), const SizedBox(width: 8),
const Text('指标分析', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), const Text('指标分析', style: TextStyle(fontSize: 19, fontWeight: FontWeight.w600)),
]), ]),
), ),
Container( Container(
@@ -598,11 +600,11 @@ class _ReportDetailPageState extends ConsumerState<ReportDetailPage> {
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
const Icon(Icons.info_outline, size: 16, color: Color(0xFFE65100)), const Icon(Icons.info_outline, size: 19, color: Color(0xFFE65100)),
const SizedBox(width: 6), const SizedBox(width: 6),
const Text( const Text(
'AI 预解读 · 待医生确认', 'AI 预解读 · 待医生确认',
style: TextStyle(fontSize: 13, color: Color(0xFFE65100), fontWeight: FontWeight.w500), style: TextStyle(fontSize: 16, color: Color(0xFFE65100), fontWeight: FontWeight.w500),
), ),
], ],
), ),
@@ -637,15 +639,15 @@ class _ReportDetailPageState extends ConsumerState<ReportDetailPage> {
child: Row(children: [ child: Row(children: [
Expanded( Expanded(
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(ind.name, style: const TextStyle(fontSize: 14)), Text(ind.name, style: const TextStyle(fontSize: 17)),
if (ind.referenceRange != null) if (ind.referenceRange != null)
Text('参考值: ${ind.referenceRange}', style: TextStyle(fontSize: 12, color: Colors.grey[400])), Text('参考值: ${ind.referenceRange}', style: TextStyle(fontSize: 15, color: Colors.grey[400])),
]), ]),
), ),
const SizedBox(width: 16), const SizedBox(width: 16),
Column(children: [ Column(children: [
Text('${ind.value} ${ind.unit}', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: statusColor)), Text('${ind.value} ${ind.unit}', style: TextStyle(fontSize: 19, fontWeight: FontWeight.w600, color: statusColor)),
Icon(statusIcon, size: 16, color: statusColor), Icon(statusIcon, size: 19, color: statusColor),
]), ]),
]), ]),
); );
@@ -660,12 +662,12 @@ class _ReportDetailPageState extends ConsumerState<ReportDetailPage> {
), ),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Row(children: [ Row(children: [
const Text('💡', style: TextStyle(fontSize: 20)), const Text('💡', style: TextStyle(fontSize: 23)),
const SizedBox(width: 8), const SizedBox(width: 8),
const Text('综合解读', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFFD97706))), const Text('综合解读', style: TextStyle(fontSize: 19, fontWeight: FontWeight.w600, color: Color(0xFFD97706))),
]), ]),
const SizedBox(height: 12), const SizedBox(height: 12),
Text(analysis.summary, style: const TextStyle(fontSize: 14, color: Color(0xFF92400E), height: 1.6)), Text(analysis.summary, style: const TextStyle(fontSize: 17, color: Color(0xFF92400E), height: 1.6)),
const SizedBox(height: 12), const SizedBox(height: 12),
Container( Container(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
@@ -673,7 +675,7 @@ class _ReportDetailPageState extends ConsumerState<ReportDetailPage> {
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
child: const Text('⚠️ AI 解读仅供参考,请以医生诊断为准', style: TextStyle(fontSize: 13, color: Color(0xFFD97706))), child: const Text('⚠️ AI 解读仅供参考,请以医生诊断为准', style: TextStyle(fontSize: 16, color: Color(0xFFD97706))),
), ),
]), ]),
); );

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../core/app_theme.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/navigation_provider.dart'; import '../../core/navigation_provider.dart';
@@ -49,7 +50,7 @@ class NotificationPrefsPage extends ConsumerWidget {
final dndOn = prefs['dndEnabled'] ?? false; final dndOn = prefs['dndEnabled'] ?? false;
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFF8F9FC), backgroundColor: AppTheme.bg,
appBar: AppBar( appBar: AppBar(
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
elevation: 0, elevation: 0,
@@ -57,7 +58,7 @@ class NotificationPrefsPage extends ConsumerWidget {
icon: const Icon(Icons.arrow_back_ios_new, color: Color(0xFF1A1A1A)), icon: const Icon(Icons.arrow_back_ios_new, color: Color(0xFF1A1A1A)),
onPressed: () => popRoute(ref), onPressed: () => popRoute(ref),
), ),
title: const Text('消息通知', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))), title: const Text('消息通知', style: TextStyle(fontSize: 21, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
centerTitle: true, centerTitle: true,
), ),
body: SingleChildScrollView( body: SingleChildScrollView(
@@ -89,7 +90,7 @@ class NotificationPrefsPage extends ConsumerWidget {
_SwitchTile( _SwitchTile(
icon: Icons.warning_amber_rounded, icon: Icons.warning_amber_rounded,
iconBg: const Color(0xFFFFEBEE), iconBg: const Color(0xFFFFEBEE),
iconColor: const Color(0xFFE53935), iconColor: AppTheme.error,
title: '健康异常提醒', title: '健康异常提醒',
subtitle: '检测到数据异常时及时通知', subtitle: '检测到数据异常时及时通知',
value: prefs['healthAlert'] ?? true, value: prefs['healthAlert'] ?? true,
@@ -107,7 +108,7 @@ class NotificationPrefsPage extends ConsumerWidget {
_SwitchTile( _SwitchTile(
icon: Icons.smart_toy_outlined, icon: Icons.smart_toy_outlined,
iconBg: const Color(0xFFF3E5F5), iconBg: const Color(0xFFF3E5F5),
iconColor: const Color(0xFF8B9CF7), iconColor: AppTheme.primary,
title: 'AI 回复通知', title: 'AI 回复通知',
subtitle: 'AI 助手回复时发送通知', subtitle: 'AI 助手回复时发送通知',
value: prefs['aiReply'] ?? false, value: prefs['aiReply'] ?? false,
@@ -133,7 +134,7 @@ class NotificationPrefsPage extends ConsumerWidget {
final picked = await showTimePicker(context: context, initialTime: const TimeOfDay(hour: 22, minute: 0)); final picked = await showTimePicker(context: context, initialTime: const TimeOfDay(hour: 22, minute: 0));
if (picked != null && context.mounted) ref.read(notificationPrefsProvider.notifier).setDndStart(picked); if (picked != null && context.mounted) ref.read(notificationPrefsProvider.notifier).setDndStart(picked);
})), })),
Padding(padding: const EdgeInsets.symmetric(horizontal: 12), child: Text('~', style: TextStyle(fontSize: 16, color: Colors.grey[400]))), Padding(padding: const EdgeInsets.symmetric(horizontal: 12), child: Text('~', style: TextStyle(fontSize: 19, color: Colors.grey[400]))),
Expanded(child: _TimeButton(label: '结束', time: '08:00', onTap: () async { Expanded(child: _TimeButton(label: '结束', time: '08:00', onTap: () async {
final picked = await showTimePicker(context: context, initialTime: const TimeOfDay(hour: 8, minute: 0)); final picked = await showTimePicker(context: context, initialTime: const TimeOfDay(hour: 8, minute: 0));
if (picked != null && context.mounted) ref.read(notificationPrefsProvider.notifier).setDndEnd(picked); if (picked != null && context.mounted) ref.read(notificationPrefsProvider.notifier).setDndEnd(picked);
@@ -157,7 +158,7 @@ class _SectionTitle extends StatelessWidget {
const _SectionTitle({required this.title}); const _SectionTitle({required this.title});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Padding(padding: const EdgeInsets.only(left: 4, bottom: 10), child: Text(title, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFF999999)))); return Padding(padding: const EdgeInsets.only(left: 4, bottom: 10), child: Text(title, style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w600, color: Color(0xFF999999))));
} }
} }
@@ -186,14 +187,14 @@ class _SwitchTile extends StatelessWidget {
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12)), decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12)),
child: Row(children: [ child: Row(children: [
if (icon != null) ...[ if (icon != null) ...[
Container(width: 38, height: 38, decoration: BoxDecoration(color: iconBg, borderRadius: BorderRadius.circular(10)), child: Icon(icon, size: 20, color: iconColor)), Container(width: 38, height: 38, decoration: BoxDecoration(color: iconBg, borderRadius: BorderRadius.circular(10)), child: Icon(icon, size: 23, color: iconColor)),
const SizedBox(width: 12), const SizedBox(width: 12),
], ],
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(title, style: const TextStyle(fontSize: 15, color: Color(0xFF1A1A1A), fontWeight: FontWeight.w500)), Text(title, style: const TextStyle(fontSize: 18, color: Color(0xFF1A1A1A), fontWeight: FontWeight.w500)),
if (subtitle != null && subtitle!.isNotEmpty) Text(subtitle!, style: TextStyle(fontSize: 12, color: Colors.grey[500])), if (subtitle != null && subtitle!.isNotEmpty) Text(subtitle!, style: TextStyle(fontSize: 15, color: Colors.grey[500])),
])), ])),
Switch(value: value, onChanged: onChanged, activeThumbColor: const Color(0xFF8B9CF7), activeTrackColor: const Color(0xFFC5BFFF)), Switch(value: value, onChanged: onChanged, activeThumbColor: AppTheme.primary, activeTrackColor: const Color(0xFFC5BFFF)),
]), ]),
); );
} }
@@ -210,7 +211,7 @@ class _TimeButton extends StatelessWidget {
return GestureDetector(onTap: onTap, child: Container( return GestureDetector(onTap: onTap, child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(border: Border.all(color: const Color(0xFFE0E0E0)), borderRadius: BorderRadius.circular(10)), decoration: BoxDecoration(border: Border.all(color: const Color(0xFFE0E0E0)), borderRadius: BorderRadius.circular(10)),
child: Column(children: [Text(label, style: TextStyle(fontSize: 11, color: Colors.grey[500])), Text(time, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF8B9CF7)))]), child: Column(children: [Text(label, style: TextStyle(fontSize: 14, color: Colors.grey[500])), Text(time, style: const TextStyle(fontSize: 19, fontWeight: FontWeight.w600, color: AppTheme.primary))]),
)); ));
} }
} }

View File

@@ -1,85 +1,70 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import '../../core/app_theme.dart';
import '../../core/navigation_provider.dart'; import '../../core/navigation_provider.dart';
import '../../providers/auth_provider.dart'; import '../../providers/auth_provider.dart';
import '../../widgets/app_menu_item.dart';
class SettingsPage extends ConsumerWidget { class SettingsPage extends ConsumerWidget {
const SettingsPage({super.key}); const SettingsPage({super.key});
@override Widget build(BuildContext context, WidgetRef ref) { @override Widget build(BuildContext context, WidgetRef ref) {
final theme = ShadTheme.of(context);
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFF8F9FC), backgroundColor: theme.colorScheme.background,
appBar: AppBar( appBar: AppBar(
backgroundColor: Colors.white, backgroundColor: theme.colorScheme.card,
elevation: 0, elevation: 0,
leading: IconButton(icon: const Icon(Icons.chevron_left), onPressed: () => popRoute(ref)), leading: IconButton(icon: Icon(LucideIcons.chevronLeft, color: theme.colorScheme.foreground), onPressed: () => popRoute(ref)),
title: const Text('设置', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))), title: Text('设置', style: TextStyle(fontSize: 21, fontWeight: FontWeight.w600, color: theme.colorScheme.foreground)),
centerTitle: true, centerTitle: true,
), ),
body: SafeArea(child: SingleChildScrollView(padding: const EdgeInsets.only(bottom: 30), child: Column(children: [ body: SafeArea(child: SingleChildScrollView(
const SizedBox(height: 12), padding: const EdgeInsets.only(bottom: 30),
_SetItem(icon: Icons.notifications_outlined, title: '消息通知', onTap: () => pushRoute(ref, 'notificationPrefs')), child: Column(children: [
_SetItem(icon: Icons.medication_outlined, title: '用药提醒', subtitle: 'mmHg / mmol/L', onTap: () => pushRoute(ref, 'medications')), const SizedBox(height: AppTheme.sMd),
_SetItem(icon: Icons.data_usage_outlined, title: '数据导出', onTap: () {}), AppMenuItem(icon: LucideIcons.bell, title: '消息通知', onTap: () => pushRoute(ref, 'notificationPrefs')),
_SetItem(icon: Icons.text_fields_outlined, title: '字体大小', trailingText: 'v1.0.0', onTap: () {}), AppMenuItem(icon: LucideIcons.pill, title: '用药提醒', subtitle: 'mmHg / mmol/L', onTap: () => pushRoute(ref, 'medications')),
_SetItem(icon: Icons.cleaning_services_outlined, title: '清除缓存', subtitle: '73.2 MB', onTap: () {}), AppMenuItem(icon: LucideIcons.database, title: '数据导出', onTap: () {}),
_SetItem(icon: Icons.info_outline, title: '关于健康管家', onTap: () => pushRoute(ref, 'staticText', params: {'type': 'about'})), AppMenuItem(icon: LucideIcons.type, title: '字体大小', trailing: 'v1.0.0', onTap: () {}),
_SetItem(icon: Icons.shield_outlined, title: '隐私协议', onTap: () => pushRoute(ref, 'staticText', params: {'type': 'privacy'})), AppMenuItem(icon: LucideIcons.sprayCan, title: '清除缓存', subtitle: '73.2 MB', onTap: () {}),
const SizedBox(height: 30), AppMenuItem(icon: LucideIcons.info, title: '关于健康管家', onTap: () => pushRoute(ref, 'staticText', params: {'type': 'about'})),
GestureDetector( AppMenuItem(icon: LucideIcons.shield, title: '隐私协议', onTap: () => pushRoute(ref, 'staticText', params: {'type': 'privacy'})),
onTap: () async { const SizedBox(height: 30),
final ok = await showDialog<bool>( GestureDetector(
context: context, onTap: () => _logout(context, ref),
builder: (ctx) => AlertDialog( child: Container(
title: const Text('退出登录'), margin: const EdgeInsets.symmetric(horizontal: AppTheme.rXl),
content: const Text('确定退出?'), height: 50,
actions: [ alignment: Alignment.center,
TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('取消')), decoration: BoxDecoration(
TextButton(onPressed: () => Navigator.pop(ctx, true), child: const Text('确定')), border: Border.all(color: theme.colorScheme.destructive.withAlpha(80)),
], borderRadius: BorderRadius.circular(AppTheme.rPill),
), ),
); child: Text('退出登录', style: TextStyle(fontSize: 19, color: theme.colorScheme.destructive, fontWeight: FontWeight.w500)),
if (ok == true) { await ref.read(authProvider.notifier).logout(); goRoute(ref, 'login'); } ),
},
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 24),
height: 50,
alignment: Alignment.center,
decoration: BoxDecoration(border: Border.all(color: const Color(0xFFE53935)), borderRadius: BorderRadius.circular(25)),
child: const Text('退出登录', style: TextStyle(fontSize: 16, color: Color(0xFFE53935), fontWeight: FontWeight.w500)),
), ),
), ]),
]))), )),
); );
} }
}
class _SetItem extends StatelessWidget { Future<void> _logout(BuildContext context, WidgetRef ref) async {
final IconData icon; final theme = ShadTheme.of(context);
final String title; final ok = await showDialog<bool>(
final String? subtitle; context: context,
final String? trailingText; builder: (ctx) => AlertDialog(
final VoidCallback? onTap; shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppTheme.rXl)),
title: Text('退出登录', style: TextStyle(color: theme.colorScheme.foreground)),
const _SetItem({required this.icon, required this.title, this.subtitle, this.trailingText, this.onTap}); content: Text('确定退出?', style: TextStyle(color: theme.colorScheme.mutedForeground)),
actions: [
@override Widget build(BuildContext context) { TextButton(onPressed: () => Navigator.pop(ctx, false), child: Text('取消', style: TextStyle(color: theme.colorScheme.mutedForeground))),
return GestureDetector( TextButton(onPressed: () => Navigator.pop(ctx, true), child: Text('确定', style: TextStyle(color: theme.colorScheme.destructive))),
onTap: onTap, ],
behavior: HitTestBehavior.opaque,
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 24, vertical: 2),
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 14),
decoration: BoxDecoration(color: Colors.white),
child: Row(children: [
Container(width: 36, height: 36, decoration: BoxDecoration(color: const Color(0xFFF0F2FF), borderRadius: BorderRadius.circular(10)), child: Icon(icon, size: 18, color: const Color(0xFF8B9CF7))),
const SizedBox(width: 12),
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [Text(title, style: const TextStyle(fontSize: 16, color: Color(0xFF1A1A1A))), if (subtitle != null && subtitle!.isNotEmpty) Text(subtitle!, style: TextStyle(fontSize: 13, color: Colors.grey[500]))])),
if (trailingText != null && trailingText!.isNotEmpty) Text(trailingText!, style: TextStyle(fontSize: 14, color: Colors.grey[400])),
if (trailingText == null || trailingText!.isEmpty) const SizedBox(),
Icon(Icons.chevron_right, size: 20, color: Colors.grey[300]),
]),
), ),
); );
if (ok == true) { await ref.read(authProvider.notifier).logout(); goRoute(ref, 'login'); }
} }
} }

View File

@@ -0,0 +1,46 @@
import 'package:flutter/material.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import '../core/app_theme.dart';
/// 统一样式的清新卡片
class AppCard extends StatelessWidget {
final Widget child;
final EdgeInsetsGeometry? padding;
final EdgeInsetsGeometry? margin;
final VoidCallback? onTap;
final Color? backgroundColor;
final BorderRadius? borderRadius;
const AppCard({
super.key,
required this.child,
this.padding = const EdgeInsets.all(AppTheme.sLg),
this.margin,
this.onTap,
this.backgroundColor,
this.borderRadius,
});
@override
Widget build(BuildContext context) {
final theme = ShadTheme.of(context);
final card = Container(
margin: margin ?? const EdgeInsets.symmetric(horizontal: AppTheme.sLg, vertical: AppTheme.sSm),
padding: padding,
decoration: BoxDecoration(
color: backgroundColor ?? theme.colorScheme.card,
borderRadius: borderRadius ?? BorderRadius.circular(AppTheme.rMd),
boxShadow: [AppTheme.shadowLight],
),
child: child,
);
if (onTap != null) {
return GestureDetector(
onTap: onTap,
child: card,
);
}
return card;
}
}

View File

@@ -0,0 +1,52 @@
import 'package:flutter/material.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
/// 统一空状态占位组件
class AppEmptyState extends StatelessWidget {
final IconData icon;
final String title;
final String? subtitle;
final Widget? action;
const AppEmptyState({
super.key,
required this.icon,
required this.title,
this.subtitle,
this.action,
});
@override
Widget build(BuildContext context) {
final theme = ShadTheme.of(context);
return Center(
child: Padding(
padding: const EdgeInsets.all(48),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: theme.colorScheme.muted,
borderRadius: BorderRadius.circular(40),
),
child: Icon(icon, size: 36, color: theme.colorScheme.mutedForeground),
),
const SizedBox(height: 20),
Text(title, style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600, color: theme.colorScheme.foreground)),
if (subtitle != null) ...[
const SizedBox(height: 6),
Text(subtitle!, style: TextStyle(fontSize: 17, color: theme.colorScheme.mutedForeground), textAlign: TextAlign.center),
],
if (action != null) ...[
const SizedBox(height: 20),
action!,
],
],
),
),
);
}
}

View File

@@ -0,0 +1,67 @@
import 'package:flutter/material.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import '../core/app_theme.dart';
/// 统一菜单项profile_page 和 settings_pages 都共用这个
class AppMenuItem extends StatelessWidget {
final IconData icon;
final String title;
final String? subtitle;
final String? trailing;
final VoidCallback? onTap;
const AppMenuItem({
super.key,
required this.icon,
required this.title,
this.subtitle,
this.trailing,
this.onTap,
});
@override
Widget build(BuildContext context) {
final theme = ShadTheme.of(context);
return GestureDetector(
onTap: onTap,
behavior: HitTestBehavior.opaque,
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 24, vertical: 2),
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 14),
decoration: BoxDecoration(
color: theme.colorScheme.card,
borderRadius: BorderRadius.circular(AppTheme.rSm),
),
child: Row(children: [
Container(
width: 38,
height: 38,
decoration: BoxDecoration(
color: AppTheme.primaryLight,
borderRadius: BorderRadius.circular(AppTheme.rSm),
),
child: Icon(icon, size: 22, color: AppTheme.primary),
),
const SizedBox(width: AppTheme.sLg),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: TextStyle(fontSize: 19, fontWeight: FontWeight.w500, color: theme.colorScheme.foreground)),
if (subtitle != null && subtitle!.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 2),
child: Text(subtitle!, style: TextStyle(fontSize: 16, color: theme.colorScheme.mutedForeground)),
),
],
),
),
if (trailing != null && trailing!.isNotEmpty)
Text(trailing!, style: TextStyle(fontSize: 17, color: theme.colorScheme.mutedForeground)),
const SizedBox(width: 4),
Icon(LucideIcons.chevronRight, size: 21, color: theme.colorScheme.mutedForeground),
]),
),
);
}
}

View File

@@ -0,0 +1,39 @@
import 'package:flutter/material.dart';
import '../core/app_theme.dart';
/// 统一样式的标签/筛选 Chip
class AppTabChip extends StatelessWidget {
final String label;
final bool selected;
final VoidCallback onTap;
const AppTabChip({
super.key,
required this.label,
required this.selected,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
margin: const EdgeInsets.only(right: AppTheme.sSm),
padding: const EdgeInsets.symmetric(horizontal: AppTheme.sLg, vertical: AppTheme.sSm),
decoration: BoxDecoration(
color: selected ? AppTheme.primary : AppTheme.primaryLight,
borderRadius: BorderRadius.circular(AppTheme.rPill),
),
child: Text(
label,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: selected ? Colors.white : AppTheme.primary,
),
),
),
);
}
}

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../core/app_theme.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../core/navigation_provider.dart'; import '../core/navigation_provider.dart';
import '../providers/auth_provider.dart'; import '../providers/auth_provider.dart';
@@ -46,12 +47,12 @@ class HealthDrawer extends ConsumerWidget {
Expanded(child: Column( Expanded(child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(user?.name ?? '未设置昵称', style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w700, color: Colors.white)), Text(user?.name ?? '未设置昵称', style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w700, color: Colors.white)),
const SizedBox(height: 2), const SizedBox(height: 2),
Text(user?.phone ?? '未登录', style: TextStyle(fontSize: 12, color: Colors.white70)), Text(user?.phone ?? '未登录', style: TextStyle(fontSize: 15, color: Colors.white70)),
], ],
)), )),
Icon(Icons.chevron_right, size: 18, color: Colors.white54), Icon(Icons.chevron_right, size: 21, color: Colors.white54),
]), ]),
), ),
), ),
@@ -72,19 +73,19 @@ class HealthDrawer extends ConsumerWidget {
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF6C5CE7).withAlpha(15), color: AppTheme.primary.withAlpha(15),
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(6),
), ),
child: const Row(mainAxisSize: MainAxisSize.min, children: [ child: const Row(mainAxisSize: MainAxisSize.min, children: [
Icon(Icons.monitor_heart_rounded, size: 13, color: Color(0xFF6C5CE7)), Icon(Icons.monitor_heart_rounded, size: 16, color: AppTheme.primary),
SizedBox(width: 4), SizedBox(width: 4),
Text('健康概览', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: Color(0xFF6C5CE7))), Text('健康概览', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: AppTheme.primary)),
]), ]),
), ),
const Spacer(), const Spacer(),
GestureDetector( GestureDetector(
onTap: () => pushRoute(ref, 'trend'), onTap: () => pushRoute(ref, 'trend'),
child: const Padding(padding: EdgeInsets.all(4), child: Text('详情', style: TextStyle(fontSize: 11, color: Color(0xFF888888)))), child: const Padding(padding: EdgeInsets.all(4), child: Text('详情', style: TextStyle(fontSize: 14, color: Color(0xFF888888)))),
), ),
]), ]),
), ),
@@ -99,7 +100,7 @@ class HealthDrawer extends ConsumerWidget {
_MiniMetric(icon: Icons.monitor_weight_outlined, label: '体重', value: _metricVal(data['Weight']), onTap: () => pushRoute(ref, 'trend', params: {'type': 'weight'})), _MiniMetric(icon: Icons.monitor_weight_outlined, label: '体重', value: _metricVal(data['Weight']), onTap: () => pushRoute(ref, 'trend', params: {'type': 'weight'})),
]), ]),
), ),
loading: () => const Padding(padding: EdgeInsets.symmetric(vertical: 20), child: Center(child: SizedBox(width: 22, height: 22, child: CircularProgressIndicator(strokeWidth: 2, color: Color(0xFF6C5CE7))))), loading: () => const Padding(padding: EdgeInsets.symmetric(vertical: 20), child: Center(child: SizedBox(width: 22, height: 22, child: CircularProgressIndicator(strokeWidth: 2, color: AppTheme.primary)))),
error: (_, __) => Padding( error: (_, __) => Padding(
padding: const EdgeInsets.fromLTRB(12, 4, 12, 14), padding: const EdgeInsets.fromLTRB(12, 4, 12, 14),
child: Row(children: [ child: Row(children: [
@@ -134,9 +135,9 @@ class HealthDrawer extends ConsumerWidget {
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(6),
), ),
child: const Row(mainAxisSize: MainAxisSize.min, children: [ child: const Row(mainAxisSize: MainAxisSize.min, children: [
Icon(Icons.apps_rounded, size: 13, color: Color(0xFFF0A060)), Icon(Icons.apps_rounded, size: 16, color: Color(0xFFF0A060)),
SizedBox(width: 4), SizedBox(width: 4),
Text('功能', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: Color(0xFFF0A060))), Text('功能', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: Color(0xFFF0A060))),
]), ]),
), ),
]), ]),
@@ -179,11 +180,11 @@ class HealthDrawer extends ConsumerWidget {
color: const Color(0xFFEEEEEE), color: const Color(0xFFEEEEEE),
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
), ),
child: const Icon(Icons.settings_outlined, size: 18, color: Color(0xFF666666)), child: const Icon(Icons.settings_outlined, size: 21, color: Color(0xFF666666)),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
const Expanded(child: Text('设置', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: Color(0xFF333333)))), const Expanded(child: Text('设置', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w500, color: Color(0xFF333333)))),
const Icon(Icons.chevron_right, size: 16, color: Color(0xFFCCCCCC)), const Icon(Icons.chevron_right, size: 19, color: Color(0xFFCCCCCC)),
]), ]),
), ),
), ),
@@ -269,7 +270,7 @@ class _MiniMetric extends StatelessWidget {
final VoidCallback? onTap; final VoidCallback? onTap;
const _MiniMetric({required this.icon, required this.label, required this.value, this.onTap}); const _MiniMetric({required this.icon, required this.label, required this.value, this.onTap});
static const _c = Color(0xFF6C5CE7); static const _c = AppTheme.primary;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -284,11 +285,11 @@ class _MiniMetric extends StatelessWidget {
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
child: Column(mainAxisSize: MainAxisSize.min, children: [ child: Column(mainAxisSize: MainAxisSize.min, children: [
Icon(icon, size: 18, color: _c), Icon(icon, size: 21, color: _c),
const SizedBox(height: 6), const SizedBox(height: 6),
Text(value, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w700, color: Color(0xFF1A1A1A)), maxLines: 1, overflow: TextOverflow.ellipsis), Text(value, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: Color(0xFF1A1A1A)), maxLines: 1, overflow: TextOverflow.ellipsis),
const SizedBox(height: 2), const SizedBox(height: 2),
Text(label, style: TextStyle(fontSize: 10, color: Colors.grey[500])), Text(label, style: TextStyle(fontSize: 13, color: Colors.grey[500])),
]), ]),
), ),
), ),
@@ -311,7 +312,7 @@ class _FeatureChip extends StatelessWidget {
required this.onTap, required this.onTap,
}); });
static const _c = Color(0xFF6C5CE7); static const _c = AppTheme.primary;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -326,9 +327,9 @@ class _FeatureChip extends StatelessWidget {
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
child: Column(mainAxisSize: MainAxisSize.min, children: [ child: Column(mainAxisSize: MainAxisSize.min, children: [
Icon(icon, size: 18, color: _c), Icon(icon, size: 21, color: _c),
const SizedBox(height: 6), const SizedBox(height: 6),
Text(label, style: const TextStyle(fontSize: 10, fontWeight: FontWeight.w500, color: Color(0xFF666666))), Text(label, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: Color(0xFF666666))),
]), ]),
), ),
), ),

View File

@@ -185,11 +185,11 @@ class ServicePackageCard extends ConsumerWidget {
color: Color(0xFFFFF8EE), color: Color(0xFFFFF8EE),
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
child: Icon(item.icon, size: 20, color: const Color(0xFFF5A623)), child: Icon(item.icon, size: 23, color: const Color(0xFFF5A623)),
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Text(item.label, Text(item.label,
style: const TextStyle(fontSize: 11, color: AppTheme.textSub), style: const TextStyle(fontSize: 14, color: AppTheme.textSub),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
], ],
@@ -212,14 +212,14 @@ class ServicePackageCard extends ConsumerWidget {
children: [ children: [
// 标题行 + 详情入口 // 标题行 + 详情入口
Row(children: [ Row(children: [
Text(package.title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: AppTheme.text)), Text(package.title, style: const TextStyle(fontSize: 19, fontWeight: FontWeight.w600, color: AppTheme.text)),
const Spacer(), const Spacer(),
GestureDetector( GestureDetector(
onTap: () => pushRoute(ref, 'servicePackageDetail', params: {'id': package.id}), onTap: () => pushRoute(ref, 'servicePackageDetail', params: {'id': package.id}),
child: Row(mainAxisSize: MainAxisSize.min, children: [ child: Row(mainAxisSize: MainAxisSize.min, children: [
Text('详情', style: TextStyle(fontSize: 13, color: AppTheme.textSub)), Text('详情', style: TextStyle(fontSize: 16, color: AppTheme.textSub)),
const SizedBox(width: 2), const SizedBox(width: 2),
Icon(Icons.chevron_right, size: 18, color: AppTheme.textHint), Icon(Icons.chevron_right, size: 21, color: AppTheme.textHint),
]), ]),
), ),
]), ]),
@@ -232,9 +232,9 @@ class ServicePackageCard extends ConsumerWidget {
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(6),
), ),
child: const Row(mainAxisSize: MainAxisSize.min, children: [ child: const Row(mainAxisSize: MainAxisSize.min, children: [
Text('VIP', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w700, color: Colors.white)), Text('VIP', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w700, color: Colors.white)),
SizedBox(width: 4), SizedBox(width: 4),
Text('VIP 产品权益', style: TextStyle(fontSize: 11, fontWeight: FontWeight.w500, color: Colors.white)), Text('VIP 产品权益', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)),
]), ]),
), ),
const SizedBox(height: 14), const SizedBox(height: 14),
@@ -246,8 +246,8 @@ class ServicePackageCard extends ConsumerWidget {
child: GestureDetector( child: GestureDetector(
onTap: () => pushRoute(ref, 'servicePackageDetail', params: {'id': package.id}), onTap: () => pushRoute(ref, 'servicePackageDetail', params: {'id': package.id}),
child: Row(mainAxisSize: MainAxisSize.min, children: [ child: Row(mainAxisSize: MainAxisSize.min, children: [
Text('查看更多服务包', style: TextStyle(fontSize: 13, color: AppTheme.primary, fontWeight: FontWeight.w500)), Text('查看更多服务包', style: TextStyle(fontSize: 16, color: AppTheme.primary, fontWeight: FontWeight.w500)),
Icon(Icons.chevron_right, size: 16, color: AppTheme.primary), Icon(Icons.chevron_right, size: 19, color: AppTheme.primary),
]), ]),
), ),
), ),

View File

@@ -41,6 +41,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.2" version: "2.1.2"
boxy:
dependency: transitive
description:
name: boxy
sha256: "42ccafe13b2893878042acc5b7e2446025328e11a3197b0bb78db42ff76aa3f0"
url: "https://pub.dev"
source: hosted
version: "2.3.0"
characters: characters:
dependency: transitive dependency: transitive
description: description:
@@ -65,6 +73,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.2" version: "1.1.2"
code_assets:
dependency: transitive
description:
name: code_assets
sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8
url: "https://pub.dev"
source: hosted
version: "1.2.1"
collection: collection:
dependency: transitive dependency: transitive
description: description:
@@ -145,6 +161,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.8" version: "2.0.8"
extended_image:
dependency: transitive
description:
name: extended_image
sha256: f6cbb1d798f51262ed1a3d93b4f1f2aa0d76128df39af18ecb77fa740f88b2e0
url: "https://pub.dev"
source: hosted
version: "10.0.1"
extended_image_library:
dependency: transitive
description:
name: extended_image_library
sha256: "1f9a24d3a00c2633891c6a7b5cab2807999eb2d5b597e5133b63f49d113811fe"
url: "https://pub.dev"
source: hosted
version: "5.0.1"
fake_async: fake_async:
dependency: transitive dependency: transitive
description: description:
@@ -230,6 +262,14 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
flutter_animate:
dependency: transitive
description:
name: flutter_animate
sha256: "7befe2d3252728afb77aecaaea1dec88a89d35b9b1d2eea6d04479e8af9117b5"
url: "https://pub.dev"
source: hosted
version: "4.5.2"
flutter_lints: flutter_lints:
dependency: "direct dev" dependency: "direct dev"
description: description:
@@ -267,6 +307,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.3.1" version: "3.3.1"
flutter_shaders:
dependency: transitive
description:
name: flutter_shaders
sha256: "34794acadd8275d971e02df03afee3dee0f98dbfb8c4837082ad0034f612a3e2"
url: "https://pub.dev"
source: hosted
version: "0.1.3"
flutter_svg:
dependency: transitive
description:
name: flutter_svg
sha256: "35882981abcbfb8c15b286f0cd690ff25bac12d95eff3e25ee207f37d4c42e7f"
url: "https://pub.dev"
source: hosted
version: "2.3.0"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
@@ -293,6 +349,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.3" version: "2.1.3"
google_fonts:
dependency: "direct main"
description:
name: google_fonts
sha256: "4e9391085e524954a51e3625b7c9c7e9851dc3f376603208bb45c24b9a66255d"
url: "https://pub.dev"
source: hosted
version: "8.1.0"
hooks:
dependency: transitive
description:
name: hooks
sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba"
url: "https://pub.dev"
source: hosted
version: "2.0.2"
http: http:
dependency: transitive dependency: transitive
description: description:
@@ -301,6 +373,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.6.0" version: "1.6.0"
http_client_helper:
dependency: transitive
description:
name: http_client_helper
sha256: "8a9127650734da86b5c73760de2b404494c968a3fd55602045ffec789dac3cb1"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
http_multi_server: http_multi_server:
dependency: transitive dependency: transitive
description: description:
@@ -397,6 +477,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.5" version: "1.0.5"
jni:
dependency: transitive
description:
name: jni
sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f
url: "https://pub.dev"
source: hosted
version: "1.0.0"
jni_flutter:
dependency: transitive
description:
name: jni_flutter
sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
js: js:
dependency: transitive dependency: transitive
description: description:
@@ -445,6 +541,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.0" version: "1.3.0"
lucide_icons_flutter:
dependency: transitive
description:
name: lucide_icons_flutter
sha256: "7c5dc01a32a9905ae34e2d84224e92d6d0c42acf8926df9e01c35a1446bf1b69"
url: "https://pub.dev"
source: hosted
version: "3.1.14+2"
markdown: markdown:
dependency: transitive dependency: transitive
description: description:
@@ -501,6 +605,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.2" version: "2.0.2"
objective_c:
dependency: transitive
description:
name: objective_c
sha256: "6cb691c686fa2838c6deb34980d426145c2a5d537491cb83d463c33cdbc726ed"
url: "https://pub.dev"
source: hosted
version: "9.4.1"
package_config: package_config:
dependency: transitive dependency: transitive
description: description:
@@ -517,6 +629,62 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.9.1" version: "1.9.1"
path_parsing:
dependency: transitive
description:
name: path_parsing
sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
path_provider:
dependency: transitive
description:
name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
url: "https://pub.dev"
source: hosted
version: "2.1.5"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd"
url: "https://pub.dev"
source: hosted
version: "2.3.1"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699"
url: "https://pub.dev"
source: hosted
version: "2.6.0"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.dev"
source: hosted
version: "2.2.1"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.dev"
source: hosted
version: "2.3.0"
petitparser: petitparser:
dependency: transitive dependency: transitive
description: description:
@@ -557,6 +725,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.0" version: "2.2.0"
record_use:
dependency: transitive
description:
name: record_use
sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed"
url: "https://pub.dev"
source: hosted
version: "0.6.0"
riverpod: riverpod:
dependency: transitive dependency: transitive
description: description:
@@ -565,6 +741,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.2.1" version: "3.2.1"
serial_csv:
dependency: transitive
description:
name: serial_csv
sha256: "2d62bb70cb3ce7251383fc86ea9aae1298ab1e57af6ef4e93b6a9751c5c268dd"
url: "https://pub.dev"
source: hosted
version: "0.5.2"
shadcn_ui:
dependency: "direct main"
description:
name: shadcn_ui
sha256: "6c06f2bcebd8734b9ed0bf3f63ef5c71981573d5664923589b0302f8280e7eaf"
url: "https://pub.dev"
source: hosted
version: "0.53.6"
shelf: shelf:
dependency: transitive dependency: transitive
description: description:
@@ -610,6 +802,22 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
slang:
dependency: transitive
description:
name: slang
sha256: "46e929158c2f563994c4d1fce5819cfa13e18b164941473d2553bcddcf387c31"
url: "https://pub.dev"
source: hosted
version: "4.15.0"
slang_flutter:
dependency: transitive
description:
name: slang_flutter
sha256: "0eb6348416a296f1bd940fe02669bcd2df5c5cfdabf893b98e448df8b7ecf4ac"
url: "https://pub.dev"
source: hosted
version: "4.15.0"
source_map_stack_trace: source_map_stack_trace:
dependency: transitive dependency: transitive
description: description:
@@ -762,6 +970,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.6.12" version: "0.6.12"
theme_extensions_builder_annotation:
dependency: transitive
description:
name: theme_extensions_builder_annotation
sha256: "75f28ac85d396d143d111a47c1395b01f3be41b7135f37bd51512921944e4206"
url: "https://pub.dev"
source: hosted
version: "7.3.0"
tuple: tuple:
dependency: transitive dependency: transitive
description: description:
@@ -770,6 +986,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.2" version: "2.0.2"
two_dimensional_scrollables:
dependency: transitive
description:
name: two_dimensional_scrollables
sha256: "4f25bd42783626c5f2810333418727455397195acb61e53710e638a6a98e0e5e"
url: "https://pub.dev"
source: hosted
version: "0.5.2"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:
@@ -778,6 +1002,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.0" version: "1.4.0"
universal_image:
dependency: transitive
description:
name: universal_image
sha256: "2eae13df84a47960cc4148fec88f09031ea64cbf667b4131ff2c907dd7b7c6d1"
url: "https://pub.dev"
source: hosted
version: "1.0.12"
uuid: uuid:
dependency: transitive dependency: transitive
description: description:
@@ -786,6 +1018,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.5.3" version: "4.5.3"
vector_graphics:
dependency: transitive
description:
name: vector_graphics
sha256: "2306c03da2ba81724afeb589c351ebbc0aa7d86005925be8f8735856dbe5e42d"
url: "https://pub.dev"
source: hosted
version: "1.2.2"
vector_graphics_codec:
dependency: transitive
description:
name: vector_graphics_codec
sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146"
url: "https://pub.dev"
source: hosted
version: "1.1.13"
vector_graphics_compiler:
dependency: transitive
description:
name: vector_graphics_compiler
sha256: "7ee12e6dffe0fc8e755179d6d91b3b34f5924223fc104d85572ef9180d73d172"
url: "https://pub.dev"
source: hosted
version: "1.2.5"
vector_math: vector_math:
dependency: transitive dependency: transitive
description: description:
@@ -850,6 +1106,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.15.0" version: "5.15.0"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
xml: xml:
dependency: transitive dependency: transitive
description: description:
@@ -868,4 +1132,4 @@ packages:
version: "3.1.3" version: "3.1.3"
sdks: sdks:
dart: ">=3.10.7 <4.0.0" dart: ">=3.10.7 <4.0.0"
flutter: ">=3.38.0" flutter: ">=3.38.4"

View File

@@ -38,6 +38,8 @@ dependencies:
# 基础图标 # 基础图标
cupertino_icons: ^1.0.8 cupertino_icons: ^1.0.8
signalr_netcore: ^1.4.4 signalr_netcore: ^1.4.4
shadcn_ui: ^0.53.6
google_fonts: ^8.1.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View File

@@ -6,8 +6,8 @@ import 'package:health_app/pages/auth/login_page.dart';
void main() { void main() {
testWidgets('主题颜色正确', (tester) async { testWidgets('主题颜色正确', (tester) async {
expect(AppTheme.primary, const Color(0xFF8B9CF7)); expect(AppTheme.primary, AppTheme.primaryLight);
expect(AppTheme.bg, const Color(0xFFF8F9FC)); expect(AppTheme.bg, AppTheme.bg);
expect(AppTheme.error, const Color(0xFFF56C6C)); expect(AppTheme.error, const Color(0xFFF56C6C));
expect(AppTheme.success, const Color(0xFF6ECF8A)); expect(AppTheme.success, const Color(0xFF6ECF8A));
}); });
@@ -38,7 +38,7 @@ void main() {
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
border: const Border(left: BorderSide(color: Color(0xFF8B9CF7), width: 3)), border: const Border(left: BorderSide(color: AppTheme.primaryLight, width: 3)),
), ),
child: const Text('收到!已记录', style: TextStyle(fontSize: 16)), child: const Text('收到!已记录', style: TextStyle(fontSize: 16)),
), ),
@@ -57,7 +57,7 @@ void main() {
constraints: const BoxConstraints(maxWidth: 300), constraints: const BoxConstraints(maxWidth: 300),
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF8B9CF7), color: AppTheme.primaryLight,
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
), ),
child: const Text('血压 135/85', style: TextStyle(fontSize: 16, color: Colors.white)), child: const Text('血压 135/85', style: TextStyle(fontSize: 16, color: Colors.white)),