Compare commits
2 Commits
16d1d3d305
...
5f8964e03f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f8964e03f | ||
|
|
58af5f6d5b |
@@ -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!,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,97 +1,159 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||||
|
|
||||||
/// 健康管家 — Lavender Breeze 淡紫清风
|
/// 健康管家 — 淡紫清新风
|
||||||
class AppTheme {
|
class AppTheme {
|
||||||
AppTheme._();
|
AppTheme._();
|
||||||
|
|
||||||
// ── 颜色 ──
|
// ── 主色调:柔和紫 ──
|
||||||
static const Color primary = Color(0xFF6C5CE7);
|
static const Color primary = Color(0xFF7C6FF7); // 柔紫
|
||||||
static const Color primaryLight = Color(0xFFEDEAFF);
|
static const Color primaryLight = Color(0xFFEDEAFF); // 浅薰衣草
|
||||||
static const Color primaryDark = Color(0xFF5A4BD1);
|
static const Color primaryDark = Color(0xFF4A3FBE); // 深紫
|
||||||
static const Color bg = Color(0xFFF8F9FC);
|
static const Color primaryGlow = Color(0xFFD9D4FF); // 发光紫
|
||||||
|
|
||||||
|
// ── 中性色 ──
|
||||||
|
static const Color bg = Color(0xFFFAF9FF); // 淡紫白
|
||||||
static const Color surface = Color(0xFFFFFFFF);
|
static const Color surface = Color(0xFFFFFFFF);
|
||||||
static const Color text = Color(0xFF2D2B32);
|
static const Color text = Color(0xFF1E1B2E); // 紫黑
|
||||||
static const Color textSub = Color(0xFF8A8892);
|
static const Color textSub = Color(0xFF6E6B82); // 灰紫
|
||||||
static const Color textHint = Color(0xFFBFBCC4);
|
static const Color textHint = Color(0xFFB5B2C8); // 浅灰紫
|
||||||
static const Color success = Color(0xFF6ECF8A);
|
static const Color border = Color(0xFFE8E6F0);
|
||||||
static const Color error = Color(0xFFF56C6C);
|
static const Color divider = Color(0xFFF4F2FA);
|
||||||
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 success = Color(0xFF52B788); // 翠绿
|
||||||
static const double radiusSm = 12;
|
static const Color error = Color(0xFFE85D75); // 柔红
|
||||||
static const double radiusMd = 16;
|
static const Color warning = Color(0xFFF5B041); // 暖琥珀
|
||||||
static const double radiusLg = 20;
|
static const Color accent = Color(0xFFA78BFA); // 中紫
|
||||||
static const double radiusXl = 24;
|
static const Color info = Color(0xFF7C6FF7); // 柔紫
|
||||||
static const double radiusPill = 999;
|
|
||||||
|
|
||||||
// ── 间距 Token ──
|
// ── 圆角 ──
|
||||||
static const double spaceXs = 4;
|
static const double rXs = 6;
|
||||||
static const double spaceSm = 8;
|
static const double rSm = 10;
|
||||||
static const double spaceMd = 12;
|
static const double rMd = 14;
|
||||||
static const double spaceLg = 16;
|
static const double rLg = 20;
|
||||||
static const double spaceXl = 20;
|
static const double rXl = 28;
|
||||||
|
static const double rPill = 999;
|
||||||
|
|
||||||
// ── 阴影 Token ──
|
// ── 间距 ──
|
||||||
static BoxShadow get shadowCard => BoxShadow(color: primary.withAlpha(15), blurRadius: 12, offset: const Offset(0, 4));
|
static const double sXs = 4;
|
||||||
static BoxShadow get shadowLight => BoxShadow(color: primary.withAlpha(10), blurRadius: 8, offset: const Offset(0, 2));
|
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(0xFFEDEAFF), // 淡紫
|
||||||
'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(0xFFF4F2FA),
|
||||||
|
secondaryForeground: text,
|
||||||
|
muted: Color(0xFFF4F2FA),
|
||||||
|
mutedForeground: textSub,
|
||||||
|
accent: accent,
|
||||||
|
accentForeground: Colors.white,
|
||||||
|
destructive: error,
|
||||||
|
destructiveForeground: Colors.white,
|
||||||
|
border: border,
|
||||||
|
input: border,
|
||||||
|
ring: primary,
|
||||||
|
selection: Color(0xFFD9D4FF),
|
||||||
|
),
|
||||||
|
radius: BorderRadius.circular(rMd),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
colors: [AppTheme.bg, AppTheme.bg, AppTheme.primaryLight],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: SafeArea(child: Center(child: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||||
|
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
Container(width: 100, height: 100, decoration: BoxDecoration(color: const Color(0xFF6C5CE7).withAlpha(20), borderRadius: BorderRadius.circular(50)), child: Stack(alignment: Alignment.center, children: [
|
// Logo
|
||||||
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))),
|
Container(
|
||||||
])),
|
width: 100, height: 100,
|
||||||
const SizedBox(height: 20),
|
decoration: BoxDecoration(
|
||||||
const Text('健康管家', style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Color(0xFF1A1A1A))),
|
color: AppTheme.primary.withAlpha(25),
|
||||||
const SizedBox(height: 8),
|
borderRadius: BorderRadius.circular(50),
|
||||||
Text('你的 AI 心脏健康管家', style: TextStyle(fontSize: 15, color: Colors.grey[500])),
|
),
|
||||||
const SizedBox(height: 36),
|
child: Stack(alignment: Alignment.center, children: [
|
||||||
TextField(controller: _phoneCtrl, keyboardType: TextInputType.phone, maxLength: 11,
|
Container(
|
||||||
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)))),
|
width: 72, height: 72,
|
||||||
const SizedBox(height: 16),
|
decoration: BoxDecoration(
|
||||||
Row(children: [
|
color: Colors.white.withAlpha(200),
|
||||||
Expanded(child: TextField(controller: _codeCtrl, keyboardType: TextInputType.number, maxLength: 6,
|
borderRadius: BorderRadius.circular(36),
|
||||||
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: ''))),
|
),
|
||||||
const SizedBox(width: 12),
|
child: const Icon(LucideIcons.heart, size: 36, color: AppTheme.primary),
|
||||||
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)))),
|
),
|
||||||
]),
|
]),
|
||||||
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))),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
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)))),
|
|
||||||
const SizedBox(height: 20),
|
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),
|
||||||
|
]),
|
||||||
|
))),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPhoneInput(ShadThemeData theme) {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
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)),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))),
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -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,
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
|
|||||||
@@ -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('保存记录'),
|
||||||
]),
|
]),
|
||||||
|
|||||||
@@ -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(
|
),
|
||||||
|
child: Row(crossAxisAlignment: CrossAxisAlignment.end, children: [
|
||||||
|
ShadButton.ghost(
|
||||||
|
size: ShadButtonSize.sm,
|
||||||
|
leading: Icon(LucideIcons.paperclip, size: 25, color: theme.colorScheme.mutedForeground),
|
||||||
|
onPressed: () => _showAttachmentPicker(context, theme),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
controller: _textCtrl,
|
controller: _textCtrl,
|
||||||
style: const TextStyle(fontSize: 15),
|
focusNode: _focusNode,
|
||||||
decoration: const InputDecoration(hintText: '输入你想说的...', hintStyle: TextStyle(fontSize: 15, color: Color(0xFFBBBBBB)), contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 10), border: InputBorder.none),
|
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(),
|
onSubmitted: (_) => _sendMessage(),
|
||||||
)),
|
),
|
||||||
IconButton(icon: const Icon(Icons.send, size: 24, color: Color(0xFF8B9CF7)), onPressed: _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(() {}); }}),
|
|
||||||
])));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
|
|||||||
@@ -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))),
|
||||||
]))),
|
]))),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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: '点击右上角添加药品',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return ListView.builder(
|
||||||
|
padding: const EdgeInsets.fromLTRB(0, 0, 0, AppTheme.sMd),
|
||||||
|
itemCount: list.length + 1,
|
||||||
|
itemBuilder: (ctx, i) {
|
||||||
if (i == list.length) return const SizedBox(height: 80);
|
if (i == list.length) return const SizedBox(height: 80);
|
||||||
final m = list[i];
|
final m = list[i];
|
||||||
final times = (m['timeOfDay'] as List?)?.map((t) => t.toString().substring(0, 5)).join(' ') ?? '';
|
final times = (m['timeOfDay'] as List?)?.map((t) => t.toString().substring(0, 5)).join(' ') ?? '';
|
||||||
final isActive = m['isActive'] == true;
|
final isActive = m['isActive'] == true;
|
||||||
return _SwipeDeleteTile(
|
final id = m['id']?.toString() ?? '';
|
||||||
key: Key(m['id']?.toString() ?? '$i'),
|
return Dismissible(
|
||||||
onDelete: () => _delete(m['id']?.toString() ?? ''),
|
key: Key(id),
|
||||||
onTap: () => pushRoute(ref, 'medCheckIn', params: {'id': m['id']?.toString() ?? ''}),
|
direction: DismissDirection.endToStart,
|
||||||
|
confirmDismiss: (_) async {
|
||||||
|
await _delete(id);
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
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(
|
child: Container(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
margin: const EdgeInsets.symmetric(horizontal: AppTheme.sLg, vertical: 4),
|
||||||
padding: const EdgeInsets.all(14),
|
padding: const EdgeInsets.all(AppTheme.sLg),
|
||||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16)),
|
decoration: BoxDecoration(
|
||||||
|
color: theme.colorScheme.card,
|
||||||
|
borderRadius: BorderRadius.circular(AppTheme.rMd),
|
||||||
|
boxShadow: [AppTheme.shadowLight],
|
||||||
|
),
|
||||||
child: Row(children: [
|
child: Row(children: [
|
||||||
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)),
|
Container(
|
||||||
const SizedBox(width: 12),
|
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: [
|
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
Row(children: [
|
Row(children: [
|
||||||
Text(m['name']?.toString() ?? '', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: isActive ? const Color(0xFF1A1A1A) : Colors.grey)),
|
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: Colors.grey[200], borderRadius: BorderRadius.circular(4)), child: const Text('已停', style: TextStyle(fontSize: 10, color: Color(0xFF999999))))],
|
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),
|
const SizedBox(height: 2),
|
||||||
Text('${m['dosage'] ?? ''} $times', style: const TextStyle(fontSize: 13, color: Color(0xFF888888))),
|
Text('${m['dosage'] ?? ''} $times', style: TextStyle(fontSize: 16, color: theme.colorScheme.mutedForeground)),
|
||||||
])),
|
])),
|
||||||
const Icon(Icons.chevron_right, size: 20, color: Color(0xFFCCCCCC)),
|
Icon(LucideIcons.chevronRight, size: 21, color: theme.colorScheme.border),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
});
|
|
||||||
},
|
},
|
||||||
)),
|
)),
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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]),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))))]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
|
child: Column(children: [
|
||||||
|
// 用户头部卡片
|
||||||
Container(
|
Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: const EdgeInsets.all(24),
|
padding: const EdgeInsets.all(AppTheme.rXl),
|
||||||
decoration: const BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: theme.colorScheme.card,
|
||||||
borderRadius: BorderRadius.only(bottomLeft: Radius.circular(24), bottomRight: Radius.circular(24)),
|
borderRadius: const BorderRadius.only(
|
||||||
|
bottomLeft: Radius.circular(AppTheme.rXl),
|
||||||
|
bottomRight: Radius.circular(AppTheme.rXl),
|
||||||
|
),
|
||||||
|
boxShadow: [AppTheme.shadowLight],
|
||||||
),
|
),
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () => pushRoute(ref, 'healthArchive'),
|
onTap: () => pushRoute(ref, 'healthArchive'),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(AppTheme.rSm),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
padding: const EdgeInsets.symmetric(vertical: AppTheme.sSm),
|
||||||
child: Row(children: [
|
child: Row(children: [
|
||||||
Stack(children: [
|
Stack(children: [
|
||||||
CircleAvatar(radius: 32, backgroundColor: const Color(0xFFEDEAFF),
|
CircleAvatar(
|
||||||
|
radius: 32,
|
||||||
|
backgroundColor: AppTheme.primaryLight,
|
||||||
backgroundImage: user?.avatarUrl != null ? NetworkImage(user!.avatarUrl!) : null,
|
backgroundImage: user?.avatarUrl != null ? NetworkImage(user!.avatarUrl!) : null,
|
||||||
child: user?.avatarUrl == null ? const Icon(Icons.person, size: 36, color: Color(0xFF6C5CE7)) : 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,
|
Positioned(
|
||||||
decoration: BoxDecoration(color: const Color(0xFF6C5CE7), borderRadius: BorderRadius.circular(10), border: Border.all(color: Colors.white, width: 2)),
|
right: 0, bottom: 0,
|
||||||
child: const Icon(Icons.edit, size: 10, color: Colors.white))),
|
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: 16),
|
const SizedBox(width: AppTheme.sLg),
|
||||||
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
Text(user?.name ?? '未设置昵称', style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Color(0xFF1A1A1A))),
|
Text(user?.name ?? '未设置昵称',
|
||||||
|
style: TextStyle(fontSize: 23, fontWeight: FontWeight.bold, color: theme.colorScheme.foreground)),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(user?.phone ?? '', style: TextStyle(fontSize: 14, color: Colors.grey[500])),
|
Text(user?.phone ?? '', style: TextStyle(fontSize: 17, color: theme.colorScheme.mutedForeground)),
|
||||||
])),
|
])),
|
||||||
Icon(Icons.chevron_right, size: 24, color: Colors.grey[400]),
|
Icon(LucideIcons.chevronRight, size: 25, color: theme.colorScheme.mutedForeground),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: AppTheme.sMd),
|
||||||
_MenuItem(icon: Icons.folder_shared, title: '健康档案', onTap: () => pushRoute(ref, 'healthArchive')),
|
AppMenuItem(icon: LucideIcons.folderArchive, title: '健康档案', onTap: () => pushRoute(ref, 'healthArchive')),
|
||||||
_MenuItem(icon: Icons.devices, title: '设备管理', onTap: () => pushRoute(ref, 'devices')),
|
AppMenuItem(icon: LucideIcons.monitorSmartphone, title: '设备管理', onTap: () => pushRoute(ref, 'devices')),
|
||||||
_MenuItem(icon: Icons.settings_outlined, title: '设置', onTap: () => pushRoute(ref, 'settings')),
|
AppMenuItem(icon: LucideIcons.settings, title: '设置', onTap: () => pushRoute(ref, 'settings')),
|
||||||
|
|
||||||
const SizedBox(height: 40),
|
const SizedBox(height: 40),
|
||||||
|
// 退出登录
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () async {
|
onTap: () => _logout(context, ref),
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: AppTheme.rXl),
|
||||||
|
height: 50,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
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)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _logout(BuildContext context, WidgetRef ref) async {
|
||||||
|
final theme = ShadTheme.of(context);
|
||||||
final ok = await showDialog<bool>(
|
final ok = await showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (ctx) => AlertDialog(
|
builder: (ctx) => AlertDialog(
|
||||||
title: const Text('退出登录'),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppTheme.rXl)),
|
||||||
content: const Text('确定退出?'),
|
title: Text('退出登录', style: TextStyle(color: theme.colorScheme.foreground)),
|
||||||
|
content: Text('确定退出?', style: TextStyle(color: theme.colorScheme.mutedForeground)),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('取消')),
|
TextButton(onPressed: () => Navigator.pop(ctx, false), child: Text('取消', style: TextStyle(color: theme.colorScheme.mutedForeground))),
|
||||||
TextButton(onPressed: () => Navigator.pop(ctx, true), child: const Text('确定')),
|
TextButton(onPressed: () => Navigator.pop(ctx, true), child: Text('确定', style: TextStyle(color: theme.colorScheme.destructive))),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (ok == true) { await ref.read(authProvider.notifier).logout(); goRoute(ref, 'login'); }
|
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 {
|
|
||||||
final IconData icon;
|
|
||||||
final String title;
|
|
||||||
final String? trailing;
|
|
||||||
final VoidCallback? onTap;
|
|
||||||
const _MenuItem({required this.icon, required this.title, this.trailing, this.onTap});
|
|
||||||
|
|
||||||
@override Widget build(BuildContext 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: 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]),
|
|
||||||
]),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))),
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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))]),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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: () {}),
|
||||||
|
AppMenuItem(icon: LucideIcons.info, title: '关于健康管家', onTap: () => pushRoute(ref, 'staticText', params: {'type': 'about'})),
|
||||||
|
AppMenuItem(icon: LucideIcons.shield, title: '隐私协议', onTap: () => pushRoute(ref, 'staticText', params: {'type': 'privacy'})),
|
||||||
const SizedBox(height: 30),
|
const SizedBox(height: 30),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () async {
|
onTap: () => _logout(context, ref),
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: AppTheme.rXl),
|
||||||
|
height: 50,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
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)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _logout(BuildContext context, WidgetRef ref) async {
|
||||||
|
final theme = ShadTheme.of(context);
|
||||||
final ok = await showDialog<bool>(
|
final ok = await showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (ctx) => AlertDialog(
|
builder: (ctx) => AlertDialog(
|
||||||
title: const Text('退出登录'),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppTheme.rXl)),
|
||||||
content: const Text('确定退出?'),
|
title: Text('退出登录', style: TextStyle(color: theme.colorScheme.foreground)),
|
||||||
|
content: Text('确定退出?', style: TextStyle(color: theme.colorScheme.mutedForeground)),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('取消')),
|
TextButton(onPressed: () => Navigator.pop(ctx, false), child: Text('取消', style: TextStyle(color: theme.colorScheme.mutedForeground))),
|
||||||
TextButton(onPressed: () => Navigator.pop(ctx, true), child: const Text('确定')),
|
TextButton(onPressed: () => Navigator.pop(ctx, true), child: Text('确定', style: TextStyle(color: theme.colorScheme.destructive))),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (ok == true) { await ref.read(authProvider.notifier).logout(); goRoute(ref, 'login'); }
|
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 {
|
|
||||||
final IconData icon;
|
|
||||||
final String title;
|
|
||||||
final String? subtitle;
|
|
||||||
final String? trailingText;
|
|
||||||
final VoidCallback? onTap;
|
|
||||||
|
|
||||||
const _SetItem({required this.icon, required this.title, this.subtitle, this.trailingText, this.onTap});
|
|
||||||
|
|
||||||
@override Widget build(BuildContext 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: 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]),
|
|
||||||
]),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
46
health_app/lib/widgets/app_card.dart
Normal file
46
health_app/lib/widgets/app_card.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
52
health_app/lib/widgets/app_empty_state.dart
Normal file
52
health_app/lib/widgets/app_empty_state.dart
Normal 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!,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
67
health_app/lib/widgets/app_menu_item.dart
Normal file
67
health_app/lib/widgets/app_menu_item.dart
Normal 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),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
39
health_app/lib/widgets/app_tab_chip.dart
Normal file
39
health_app/lib/widgets/app_tab_chip.dart
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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))),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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)),
|
||||||
|
|||||||
Reference in New Issue
Block a user