feat: UI全面改造 + 后端多项修复
【后端修复】 - 删除diet/consultation agent假工具声明 - 修复DayOfWeek实体注释 - 修复Vision API content序列化 - cleanup_service级联删除修复 - 用药提醒时区偏差修复 - 统一DateTime处理(UtcNow+8) - 新增UTC DateTime JSON转换器 【前端UI重构】 - 配色体系全面更新(#8B5CF6淡紫+#F0ECFF背景) - 登录页重设计 - 首页重设计(透明顶栏、渐变背景、胶囊输入区) - 聊天卡片加白蓝边框、渐变标题 - 侧边栏重构(渐变背景、合并顶部、删除底部设置) - 确认卡片可编辑字段恢复 - 所有子页面加返回按钮 - catch异常加日志 - 删除后refresh provider缓存
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
import '../../core/app_colors.dart';
|
||||
import '../../core/app_theme.dart';
|
||||
import '../../core/navigation_provider.dart';
|
||||
import '../../providers/auth_provider.dart';
|
||||
import '../../providers/data_providers.dart';
|
||||
|
||||
class LoginPage extends ConsumerStatefulWidget {
|
||||
const LoginPage({super.key});
|
||||
@@ -46,12 +48,15 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
||||
final err = await ref.read(authProvider.notifier).login(_phoneCtrl.text.trim(), _codeCtrl.text.trim());
|
||||
setState(() => _loading = false);
|
||||
if (err != null) { setState(() => _error = err); return; }
|
||||
ref.invalidate(latestHealthProvider);
|
||||
ref.invalidate(medicationListProvider);
|
||||
ref.invalidate(medicationReminderProvider);
|
||||
ref.invalidate(currentExercisePlanProvider);
|
||||
goRoute(ref, 'home');
|
||||
}
|
||||
|
||||
@override Widget build(BuildContext context) {
|
||||
final authState = ref.watch(authProvider);
|
||||
final theme = ShadTheme.of(context);
|
||||
if (authState.isLoggedIn && !authState.isLoading) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => goRoute(ref, 'home'));
|
||||
}
|
||||
@@ -60,190 +65,150 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
||||
body: Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [AppTheme.bg, AppTheme.bg, AppTheme.primaryLight],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Color(0xFFEEF2FF), Color(0xFFF5F3FF)],
|
||||
),
|
||||
),
|
||||
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: 40),
|
||||
// Logo
|
||||
Container(
|
||||
width: 100, height: 100,
|
||||
width: 88, height: 88,
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.primary.withAlpha(25),
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(44),
|
||||
boxShadow: [BoxShadow(color: AppColors.primary.withOpacity(0.15), blurRadius: 20, offset: const Offset(0, 8))],
|
||||
),
|
||||
child: Stack(alignment: Alignment.center, children: [
|
||||
Container(
|
||||
width: 72, height: 72,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withAlpha(200),
|
||||
borderRadius: BorderRadius.circular(36),
|
||||
child: const Icon(LucideIcons.heart, size: 40, color: AppColors.primary),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const Text('健康管家', style: TextStyle(fontSize: 28, fontWeight: FontWeight.w700, color: AppColors.textPrimary)),
|
||||
const SizedBox(height: 6),
|
||||
const Text('你的 AI 心脏健康管家', style: TextStyle(fontSize: 15, color: AppColors.textSecondary)),
|
||||
const SizedBox(height: 40),
|
||||
|
||||
// 手机号
|
||||
Container(
|
||||
height: 52,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.backgroundSoft,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Row(children: [
|
||||
const Padding(padding: EdgeInsets.only(left: 16), child: Text('+86', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: AppColors.textPrimary))),
|
||||
Container(width: 1, height: 24, color: AppColors.border, margin: const EdgeInsets.symmetric(horizontal: 12)),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _phoneCtrl,
|
||||
keyboardType: TextInputType.phone,
|
||||
maxLength: 11,
|
||||
style: const TextStyle(fontSize: 16, color: AppColors.textPrimary),
|
||||
decoration: const InputDecoration(
|
||||
hintText: '请输入手机号',
|
||||
hintStyle: TextStyle(fontSize: 15, color: AppColors.textHint),
|
||||
border: InputBorder.none,
|
||||
counterText: '',
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 14),
|
||||
),
|
||||
),
|
||||
child: const Icon(LucideIcons.heart, size: 36, color: AppTheme.primary),
|
||||
),
|
||||
]),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text('健康管家', style: TextStyle(fontSize: 32, fontWeight: FontWeight.bold, color: theme.colorScheme.foreground)),
|
||||
const SizedBox(height: 8),
|
||||
Text('你的 AI 心脏健康管家', style: TextStyle(fontSize: 18, color: theme.colorScheme.mutedForeground)),
|
||||
const SizedBox(height: 36),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 手机号输入
|
||||
_buildPhoneInput(theme),
|
||||
const SizedBox(height: AppTheme.sLg),
|
||||
// 验证码
|
||||
Row(children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
height: 52,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.backgroundSoft,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: TextField(
|
||||
controller: _codeCtrl,
|
||||
keyboardType: TextInputType.number,
|
||||
maxLength: 6,
|
||||
style: const TextStyle(fontSize: 16, color: AppColors.textPrimary),
|
||||
decoration: const InputDecoration(
|
||||
hintText: '验证码',
|
||||
hintStyle: TextStyle(fontSize: 15, color: AppColors.textHint),
|
||||
border: InputBorder.none,
|
||||
counterText: '',
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
GestureDetector(
|
||||
onTap: (_countdown > 0 || _sending) ? null : _sendSms,
|
||||
child: Container(
|
||||
width: 120, height: 52,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: (_countdown > 0 || _sending) ? AppColors.backgroundSoft : AppColors.primary,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Text(
|
||||
_sending ? '发送中' : _countdown > 0 ? '${_countdown}s' : '获取验证码',
|
||||
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500,
|
||||
color: (_countdown > 0 || _sending) ? AppColors.textHint : Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
]),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// 验证码输入
|
||||
_buildCodeRow(theme),
|
||||
const SizedBox(height: AppTheme.sSm),
|
||||
// 协议
|
||||
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 ? AppColors.primary : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(color: _agreed ? AppColors.primary : AppColors.border, width: 1.5),
|
||||
),
|
||||
child: _agreed ? const Icon(Icons.check, size: 14, color: Colors.white) : null,
|
||||
),
|
||||
RichText(text: TextSpan(children: [
|
||||
TextSpan(text: '已阅读并同意', style: TextStyle(fontSize: 13, color: AppColors.textHint)),
|
||||
TextSpan(text: '《服务协议》', style: TextStyle(fontSize: 13, color: AppColors.primary)),
|
||||
TextSpan(text: '和', style: TextStyle(fontSize: 13, color: AppColors.textHint)),
|
||||
TextSpan(text: '《隐私政策》', style: TextStyle(fontSize: 13, color: AppColors.primary)),
|
||||
])),
|
||||
]),
|
||||
),
|
||||
|
||||
// 协议勾选
|
||||
_buildAgreement(theme),
|
||||
|
||||
// 错误提示
|
||||
if (_error != null)
|
||||
Padding(padding: const EdgeInsets.only(top: AppTheme.sMd), child: Text(_error!, style: TextStyle(color: theme.colorScheme.destructive, fontSize: 16))),
|
||||
Padding(padding: const EdgeInsets.only(top: 12), child: Text(_error!, style: const TextStyle(color: AppColors.error, fontSize: 14))),
|
||||
|
||||
const SizedBox(height: AppTheme.rXl),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// 登录按钮
|
||||
_buildLoginButton(theme),
|
||||
GestureDetector(
|
||||
onTap: _loading ? null : _login,
|
||||
child: Container(
|
||||
width: double.infinity, height: 52, alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
gradient: AppColors.primaryGradient,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: _loading
|
||||
? const SizedBox(width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2.5, color: Colors.white))
|
||||
: const Text('登 录', style: TextStyle(fontSize: 18, color: Colors.white, fontWeight: FontWeight.w600, letterSpacing: 2)),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: AppTheme.sXl),
|
||||
const SizedBox(height: 40),
|
||||
]),
|
||||
))),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ class _TrendPageState extends ConsumerState<TrendPage> {
|
||||
'isAbnormal': r['isAbnormal'] == true,
|
||||
});
|
||||
}
|
||||
} catch (_) {}
|
||||
} catch (e) { debugPrint('[Trend] 加载趋势失败: $e'); }
|
||||
}
|
||||
all.sort((a, b) => (b['date'] as DateTime).compareTo(a['date'] as DateTime));
|
||||
if (mounted) {
|
||||
|
||||
@@ -40,9 +40,9 @@ class _DeviceScanPageState extends ConsumerState<DeviceScanPage> {
|
||||
|
||||
await [Permission.bluetoothScan, Permission.bluetoothConnect].request();
|
||||
|
||||
try { await FlutterBluePlus.turnOn(); } catch (_) {}
|
||||
try { await FlutterBluePlus.turnOn(); } catch (e) { debugPrint('[BLE Scan] 操作失败: $e'); }
|
||||
|
||||
try { await FlutterBluePlus.stopScan(); } catch (_) {}
|
||||
try { await FlutterBluePlus.stopScan(); } catch (e) { debugPrint('[BLE Scan] 操作失败: $e'); }
|
||||
await FlutterBluePlus.startScan(
|
||||
timeout: const Duration(seconds: 30),
|
||||
androidScanMode: AndroidScanMode.lowLatency,
|
||||
@@ -72,7 +72,7 @@ class _DeviceScanPageState extends ConsumerState<DeviceScanPage> {
|
||||
final api = ref.read(apiClientProvider);
|
||||
await api.post('/api/health-records', data: reading.toHealthRecord());
|
||||
await ref.read(omronDeviceProvider.notifier).onReading(reading);
|
||||
} catch (_) {}
|
||||
} catch (e) { debugPrint('[BLE Scan] 操作失败: $e'); }
|
||||
});
|
||||
|
||||
if (mounted) {
|
||||
@@ -102,7 +102,7 @@ class _DeviceScanPageState extends ConsumerState<DeviceScanPage> {
|
||||
body: Column(children: [
|
||||
Container(
|
||||
width: double.infinity, padding: const EdgeInsets.all(16),
|
||||
color: AppColors.iconBackground,
|
||||
color: AppColors.iconBg,
|
||||
child: Row(children: [
|
||||
if (_scanning) ...[
|
||||
const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2, color: Color(0xFF5B8DEF))),
|
||||
@@ -128,7 +128,7 @@ class _DeviceScanPageState extends ConsumerState<DeviceScanPage> {
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: const Text('重新扫描'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: AppColors.iconColor,
|
||||
foregroundColor: AppColors.primary,
|
||||
side: const BorderSide(color: Color(0xFF5B8DEF)),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
@@ -152,13 +152,13 @@ class _DeviceScanPageState extends ConsumerState<DeviceScanPage> {
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.cardBackground, borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: AppColors.cardShadow,
|
||||
border: isBP ? Border.all(color: AppColors.iconColor, width: 1.5) : null,
|
||||
border: isBP ? Border.all(color: AppColors.primary, width: 1.5) : null,
|
||||
),
|
||||
child: Row(children: [
|
||||
Container(
|
||||
width: 44, height: 44,
|
||||
decoration: BoxDecoration(color: isBP ? AppColors.iconBackground : AppColors.backgroundSecondary, borderRadius: BorderRadius.circular(12)),
|
||||
child: Icon(Icons.bluetooth, size: 24, color: isBP ? AppColors.iconColor : AppColors.textHint),
|
||||
decoration: BoxDecoration(color: isBP ? AppColors.iconBg : AppColors.cardInner, borderRadius: BorderRadius.circular(12)),
|
||||
child: Icon(Icons.bluetooth, size: 24, color: isBP ? AppColors.primary : AppColors.textHint),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
@@ -167,7 +167,7 @@ class _DeviceScanPageState extends ConsumerState<DeviceScanPage> {
|
||||
Flexible(child: Text(name, style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w600, color: AppColors.textPrimary))),
|
||||
if (isBP) ...[
|
||||
const SizedBox(width: 6),
|
||||
Container(padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration(color: AppColors.iconColor, borderRadius: BorderRadius.circular(4)), child: const Text('血压计', style: TextStyle(fontSize: 11, color: Colors.white))),
|
||||
Container(padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration(color: AppColors.primary, borderRadius: BorderRadius.circular(4)), child: const Text('血压计', style: TextStyle(fontSize: 11, color: Colors.white))),
|
||||
],
|
||||
]),
|
||||
const SizedBox(height: 2),
|
||||
@@ -179,7 +179,7 @@ class _DeviceScanPageState extends ConsumerState<DeviceScanPage> {
|
||||
else
|
||||
GestureDetector(
|
||||
onTap: () => _connect(r),
|
||||
child: Container(padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration(gradient: AppColors.purpleBlueGradient, borderRadius: BorderRadius.circular(12)), child: const Text('连接', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: Colors.white))),
|
||||
child: Container(padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration(gradient: AppColors.primaryGradient, borderRadius: BorderRadius.circular(12)), child: const Text('连接', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: Colors.white))),
|
||||
),
|
||||
]),
|
||||
);
|
||||
@@ -202,7 +202,7 @@ class _DeviceScanPageState extends ConsumerState<DeviceScanPage> {
|
||||
Widget _tip(String num, String text) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Container(width: 20, height: 20, alignment: Alignment.center, decoration: BoxDecoration(color: AppColors.iconColor, borderRadius: BorderRadius.circular(10)), child: Text(num, style: const TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.w600))),
|
||||
Container(width: 20, height: 20, alignment: Alignment.center, decoration: BoxDecoration(color: AppColors.primary, borderRadius: BorderRadius.circular(10)), child: Text(num, style: const TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.w600))),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(child: Text(text, style: TextStyle(fontSize: 14, color: AppColors.textSecondary))),
|
||||
]),
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import '../../core/navigation_provider.dart';
|
||||
import '../../core/app_colors.dart';
|
||||
import '../../core/app_theme.dart';
|
||||
import '../../providers/auth_provider.dart';
|
||||
import '../../utils/sse_handler.dart';
|
||||
@@ -123,7 +124,9 @@ class DietNotifier extends Notifier<DietState> {
|
||||
if (text.isNotEmpty) state = state.copyWith(commentary: text.trim());
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
} catch (e) {
|
||||
debugPrint('[Diet] 获取饮食点评失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
List<FoodItem> _parseFoodItems(String raw) {
|
||||
@@ -243,7 +246,7 @@ class _DietCapturePageState extends ConsumerState<DietCapturePage> {
|
||||
centerTitle: true,
|
||||
),
|
||||
body: state.imagePath == null
|
||||
? const Center(child: Text('请从首页拍摄或选择食物照片', style: TextStyle(color: Color(0xFF999999))))
|
||||
? const Center(child: Text('请从首页拍摄或选择食物照片', style: TextStyle(color: AppColors.textSecondary)))
|
||||
: _buildResultView(context, ref),
|
||||
);
|
||||
}
|
||||
@@ -263,11 +266,12 @@ class _DietCapturePageState extends ConsumerState<DietCapturePage> {
|
||||
final totalCalories = state.foods.where((f) => f.selected).fold(0, (sum, f) => sum + f.calories);
|
||||
|
||||
return Container(
|
||||
color: _kPageBg,
|
||||
color: AppColors.background,
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(children: [
|
||||
_buildImagePreview(state.imagePath!),
|
||||
const SizedBox(height: 20),
|
||||
const SizedBox(height: 16),
|
||||
_buildMealSelector(ref),
|
||||
const SizedBox(height: 16),
|
||||
if (state.isAnalyzing)
|
||||
@@ -282,8 +286,8 @@ class _DietCapturePageState extends ConsumerState<DietCapturePage> {
|
||||
_buildAiCommentary(state.commentary!),
|
||||
],
|
||||
],
|
||||
const SizedBox(height: 24),
|
||||
_buildSubmitButton(context, ref),
|
||||
const SizedBox(height: 20),
|
||||
_buildSubmitButton(),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
]),
|
||||
@@ -293,29 +297,25 @@ class _DietCapturePageState extends ConsumerState<DietCapturePage> {
|
||||
|
||||
// ─────────── 图片预览 ───────────
|
||||
Widget _buildImagePreview(String path) {
|
||||
return Stack(
|
||||
children: [
|
||||
Container(
|
||||
height: 220,
|
||||
return Center(
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Container(
|
||||
height: 200,
|
||||
width: MediaQuery.of(context).size.width * 0.75,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFE0E0E0),
|
||||
image: DecorationImage(image: FileImage(File(path)), fit: BoxFit.cover),
|
||||
boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 14, offset: const Offset(0, 4))],
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 0, right: 0, bottom: 0,
|
||||
child: Container(
|
||||
height: 60,
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [Colors.transparent, Color(0x40000000)],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
foregroundDecoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [Colors.transparent, Colors.black38],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -351,9 +351,9 @@ class _DietCapturePageState extends ConsumerState<DietCapturePage> {
|
||||
: null,
|
||||
),
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
Icon(m.icon, size: 23, color: isSelected ? Colors.white : _kSubText),
|
||||
Icon(m.icon, size: 23, color: isSelected ? AppColors.textOnGradient : _kSubText),
|
||||
const SizedBox(height: 4),
|
||||
Text(m.label, style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: isSelected ? Colors.white : _kSubText)),
|
||||
Text(m.label, style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: isSelected ? AppColors.textOnGradient : _kSubText)),
|
||||
]),
|
||||
),
|
||||
),
|
||||
@@ -416,7 +416,7 @@ class _DietCapturePageState extends ConsumerState<DietCapturePage> {
|
||||
const Spacer(),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(color: const Color(0xFFFFF8EE), borderRadius: BorderRadius.circular(8)),
|
||||
decoration: BoxDecoration(color: AppColors.warningLight, borderRadius: BorderRadius.circular(8)),
|
||||
child: Text('共 $totalCal kcal', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: _kWarning)),
|
||||
),
|
||||
IconButton(
|
||||
@@ -441,9 +441,9 @@ class _DietCapturePageState extends ConsumerState<DietCapturePage> {
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
decoration: BoxDecoration(
|
||||
color: food.selected ? const Color(0xFFF6F4FF) : const Color(0xFFFAFAFA),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: food.selected ? _kPrimary.withAlpha(30) : const Color(0xFFEEEEEE)),
|
||||
color: food.selected ? AppColors.iconBg : AppColors.background,
|
||||
borderRadius: BorderRadius.circular(AppTheme.rMd),
|
||||
border: Border.all(color: food.selected ? _kPrimary.withAlpha(30) : AppColors.border),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 10, 6, 10),
|
||||
@@ -454,10 +454,10 @@ class _DietCapturePageState extends ConsumerState<DietCapturePage> {
|
||||
width: 22, height: 22,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: food.selected ? _kPrimary : Colors.white,
|
||||
border: Border.all(color: food.selected ? _kPrimary : const Color(0xFFCCCCCC), width: 2),
|
||||
color: food.selected ? _kPrimary : AppTheme.surface,
|
||||
border: Border.all(color: food.selected ? _kPrimary : AppColors.border, width: 2),
|
||||
),
|
||||
child: food.selected ? const Icon(Icons.check, size: 17, color: Colors.white) : null,
|
||||
child: food.selected ? const Icon(Icons.check, size: 17, color: AppColors.textOnGradient) : null,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
@@ -514,7 +514,7 @@ class _DietCapturePageState extends ConsumerState<DietCapturePage> {
|
||||
]),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close, size: 19, color: Color(0xFFBBBBBB)),
|
||||
icon: const Icon(Icons.close, size: 19, color: AppColors.textHint),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
onPressed: () => ref.read(dietProvider.notifier).removeFood(food.id),
|
||||
@@ -532,7 +532,7 @@ class _DietCapturePageState extends ConsumerState<DietCapturePage> {
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(colors: [_kPrimary, Color(0xFF8B7CF0)], begin: Alignment.topLeft, end: Alignment.bottomRight),
|
||||
gradient: AppColors.primaryGradient,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [BoxShadow(color: _kPrimary.withAlpha(40), blurRadius: 10, offset: const Offset(0, 4))],
|
||||
),
|
||||
@@ -548,12 +548,12 @@ class _DietCapturePageState extends ConsumerState<DietCapturePage> {
|
||||
value: (totalCalories / 700).clamp(0.0, 1.0),
|
||||
strokeWidth: 4,
|
||||
backgroundColor: Colors.white24,
|
||||
color: Colors.white,
|
||||
color: AppColors.textOnGradient,
|
||||
),
|
||||
),
|
||||
Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
Text('$totalCalories', style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w800, color: Colors.white)),
|
||||
Text('kcal', style: const TextStyle(fontSize: 13, color: Colors.white70)),
|
||||
Text('$totalCalories', style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w800, color: AppColors.textOnGradient)),
|
||||
Text('kcal', style: const TextStyle(fontSize: 13, color: AppColors.textOnGradient)),
|
||||
]),
|
||||
],
|
||||
),
|
||||
@@ -561,14 +561,14 @@ class _DietCapturePageState extends ConsumerState<DietCapturePage> {
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
const Text('本餐热量', style: TextStyle(fontSize: 17, color: Colors.white70)),
|
||||
const Text('本餐热量', style: TextStyle(fontSize: 17, color: AppColors.textOnGradient)),
|
||||
const SizedBox(height: 4),
|
||||
Row(children: [
|
||||
Expanded(child: _macroBar('碳水', 0.55, const Color(0xFFFFF9C4))),
|
||||
Expanded(child: _macroBar('碳水', 0.55, AppColors.warningLight)),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: _macroBar('蛋白', 0.25, const Color(0xFFD4C8FC))),
|
||||
Expanded(child: _macroBar('蛋白', 0.25, AppColors.iconBg)),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: _macroBar('脂肪', 0.20, const Color(0xFFFFCDD2))),
|
||||
Expanded(child: _macroBar('脂肪', 0.20, AppColors.errorLight)),
|
||||
]),
|
||||
]),
|
||||
),
|
||||
@@ -582,7 +582,7 @@ class _DietCapturePageState extends ConsumerState<DietCapturePage> {
|
||||
Row(children: [
|
||||
Container(width: 6, height: 6, decoration: BoxDecoration(color: color, shape: BoxShape.circle)),
|
||||
const SizedBox(width: 4),
|
||||
Text(label, style: const TextStyle(fontSize: 13, color: Colors.white70)),
|
||||
Text(label, style: const TextStyle(fontSize: 13, color: AppColors.textOnGradient)),
|
||||
]),
|
||||
const SizedBox(height: 3),
|
||||
ClipRRect(
|
||||
@@ -597,19 +597,22 @@ class _DietCapturePageState extends ConsumerState<DietCapturePage> {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: _kSurface,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: _kBorder),
|
||||
gradient: AppColors.gLightPurple,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [BoxShadow(color: AppTheme.primary.withAlpha(20), blurRadius: 12, offset: const Offset(0, 4))],
|
||||
),
|
||||
child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Container(
|
||||
width: 32, height: 32,
|
||||
decoration: BoxDecoration(color: _kPrimaryLight, borderRadius: BorderRadius.circular(8)),
|
||||
child: const Icon(Icons.auto_awesome, size: 19, color: _kPrimary),
|
||||
width: 36, height: 36,
|
||||
decoration: BoxDecoration(
|
||||
gradient: AppColors.primaryGradient,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(10)),
|
||||
),
|
||||
child: const Icon(Icons.auto_awesome, size: 20, color: AppColors.textOnGradient),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: Text(text, style: const TextStyle(fontSize: 16, color: _kText, height: 1.6))),
|
||||
]),
|
||||
),
|
||||
@@ -617,47 +620,34 @@ class _DietCapturePageState extends ConsumerState<DietCapturePage> {
|
||||
}
|
||||
|
||||
// ─────────── 保存按钮 ───────────
|
||||
Widget _buildSubmitButton(BuildContext context, WidgetRef ref) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
height: 50,
|
||||
child: ElevatedButton(
|
||||
onPressed: () async {
|
||||
try {
|
||||
await ref.read(dietProvider.notifier).saveRecord();
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: const Text('饮食记录已保存'),
|
||||
backgroundColor: _kPrimary,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
duration: const Duration(seconds: 2),
|
||||
));
|
||||
popRoute(ref);
|
||||
}
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text('保存失败: $e'),
|
||||
backgroundColor: AppTheme.error,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
));
|
||||
}
|
||||
}
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: _kPrimary,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppTheme.rMd)),
|
||||
textStyle: const TextStyle(fontSize: 19, fontWeight: FontWeight.w600),
|
||||
Widget _buildSubmitButton() {
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
height: 52,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: AppColors.gPurplePink,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
boxShadow: [BoxShadow(color: AppColors.primary.withAlpha(20), blurRadius: 12, offset: const Offset(0, 4))],
|
||||
),
|
||||
padding: const EdgeInsets.all(1.5),
|
||||
child: Material(
|
||||
color: AppTheme.surface,
|
||||
borderRadius: BorderRadius.circular(12.5),
|
||||
child: InkWell(
|
||||
onTap: () async {
|
||||
try {
|
||||
await ref.read(dietProvider.notifier).saveRecord();
|
||||
if (mounted) popRoute(ref);
|
||||
} catch (e) { debugPrint('[Diet] 保存记录失败: $e'); }
|
||||
},
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: const Row(mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||
Icon(Icons.check_circle_outline, size: 22, color: AppColors.primary),
|
||||
SizedBox(width: 8),
|
||||
Text('保存记录', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: AppColors.primary)),
|
||||
]),
|
||||
),
|
||||
child: const Row(mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||
Icon(Icons.check_circle_outline, size: 23),
|
||||
SizedBox(width: 8),
|
||||
Text('保存记录'),
|
||||
]),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
import '../../core/app_colors.dart';
|
||||
import '../../core/app_theme.dart';
|
||||
import '../../core/navigation_provider.dart';
|
||||
import '../../providers/auth_provider.dart';
|
||||
@@ -24,6 +25,7 @@ class _HomePageState extends ConsumerState<HomePage> {
|
||||
String? _pickedImagePath;
|
||||
final Set<ActiveAgent> _welcomedAgents = {};
|
||||
final _focusNode = FocusNode();
|
||||
int _lastMsgCount = 0;
|
||||
|
||||
@override void initState() { super.initState(); }
|
||||
@override void dispose() { _textCtrl.dispose(); _scrollCtrl.dispose(); _focusNode.dispose(); super.dispose(); }
|
||||
@@ -33,7 +35,7 @@ class _HomePageState extends ConsumerState<HomePage> {
|
||||
final imagePath = _pickedImagePath;
|
||||
if (text.isEmpty && imagePath == null) return;
|
||||
_textCtrl.clear();
|
||||
_focusNode.unfocus(); // 收起键盘
|
||||
_focusNode.unfocus();
|
||||
setState(() => _pickedImagePath = null);
|
||||
if (imagePath != null) {
|
||||
ref.read(chatProvider.notifier).sendImage(imagePath, text);
|
||||
@@ -46,8 +48,6 @@ class _HomePageState extends ConsumerState<HomePage> {
|
||||
final chatState = ref.watch(chatProvider);
|
||||
final auth = ref.watch(authProvider);
|
||||
final user = auth.user;
|
||||
final selectedAgent = ref.watch(selectedAgentProvider);
|
||||
final theme = ShadTheme.of(context);
|
||||
|
||||
ref.listen(cameraActionProvider, (prev, next) {
|
||||
if (next == 'camera') { _pickImage(ImageSource.camera); ref.read(cameraActionProvider.notifier).clear(); }
|
||||
@@ -58,49 +58,52 @@ class _HomePageState extends ConsumerState<HomePage> {
|
||||
else if (next == 'pickFoodGallery') { _pickFoodImage(ImageSource.gallery); ref.read(dietActionProvider.notifier).clear(); }
|
||||
});
|
||||
|
||||
final currentCount = chatState.messages.length;
|
||||
if (currentCount > _lastMsgCount) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (_scrollCtrl.hasClients) _scrollCtrl.jumpTo(0);
|
||||
});
|
||||
}
|
||||
_lastMsgCount = currentCount;
|
||||
|
||||
return Scaffold(
|
||||
drawer: const HealthDrawer(),
|
||||
backgroundColor: theme.colorScheme.background,
|
||||
body: SafeArea(
|
||||
body: Container(
|
||||
decoration: const BoxDecoration(gradient: AppColors.bgGradient),
|
||||
child: SafeArea(
|
||||
child: Column(children: [
|
||||
_buildHeader(user, theme),
|
||||
_buildHeader(user),
|
||||
Expanded(child: ChatMessagesView(scrollCtrl: _scrollCtrl, messages: chatState.messages)),
|
||||
_buildBottomBar(context, selectedAgent, theme),
|
||||
_buildBottomBar(context),
|
||||
]),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ═══════ 顶部栏 ═══════
|
||||
Widget _buildHeader(dynamic user, ShadThemeData theme) {
|
||||
// ═══════ 顶部栏 — 透明背景,与页面渐变无缝衔接 ═══════
|
||||
Widget _buildHeader(dynamic user) {
|
||||
return Container(
|
||||
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)),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||
child: Row(children: [
|
||||
Builder(builder: (ctx) => GestureDetector(
|
||||
onTap: () => Scaffold.of(ctx).openDrawer(),
|
||||
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,
|
||||
child: Container(
|
||||
width: 38, height: 38,
|
||||
decoration: BoxDecoration(color: AppColors.iconBg, borderRadius: BorderRadius.circular(10)),
|
||||
child: const Icon(LucideIcons.menu, size: 20, color: AppColors.primary),
|
||||
),
|
||||
)),
|
||||
const SizedBox(width: 10),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
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 Text('AI 健康管家', style: TextStyle(fontSize: 13, color: AppColors.textHint)),
|
||||
Text('${_getGreeting()},${(user?.name != null && user!.name!.isNotEmpty) ? user.name : '用户'}!',
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: theme.colorScheme.foreground)),
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: AppColors.textPrimary)),
|
||||
])),
|
||||
Icon(LucideIcons.bell, size: 25, color: theme.colorScheme.mutedForeground),
|
||||
GestureDetector(
|
||||
onTap: () => pushRoute(ref, 'notificationPrefs'),
|
||||
child: const Icon(LucideIcons.bell, size: 22, color: AppColors.textSecondary),
|
||||
),
|
||||
]),
|
||||
);
|
||||
}
|
||||
@@ -113,7 +116,7 @@ class _HomePageState extends ConsumerState<HomePage> {
|
||||
return '晚上好';
|
||||
}
|
||||
|
||||
// ═══════ 智能体选择条 ═══════
|
||||
// ═══════ 智能体胶囊 ═══════
|
||||
static final _agentDefs = [
|
||||
(ActiveAgent.consultation, '问诊', LucideIcons.messageCircle),
|
||||
(ActiveAgent.health, '记数据', LucideIcons.heart),
|
||||
@@ -123,32 +126,29 @@ class _HomePageState extends ConsumerState<HomePage> {
|
||||
(ActiveAgent.exercise, '运动', LucideIcons.trendingUp),
|
||||
];
|
||||
|
||||
Widget _buildAgentBar(ActiveAgent? selected, ShadThemeData theme) {
|
||||
return Container(
|
||||
height: 40,
|
||||
padding: const EdgeInsets.symmetric(horizontal: AppTheme.sMd),
|
||||
Widget _buildAgentBar() {
|
||||
return SizedBox(
|
||||
height: 32,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
itemCount: _agentDefs.length,
|
||||
separatorBuilder: (_, i) => const SizedBox(width: 6),
|
||||
separatorBuilder: (_, i) => const SizedBox(width: 8),
|
||||
itemBuilder: (_, i) {
|
||||
final (agent, label, icon) = _agentDefs[i];
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
// 用户标签 → 欢迎卡片,不发 AI 后端
|
||||
ref.read(chatProvider.notifier).triggerAgent(agent, label);
|
||||
},
|
||||
onTap: () => ref.read(chatProvider.notifier).triggerAgent(agent, label),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.card,
|
||||
borderRadius: BorderRadius.circular(AppTheme.rPill),
|
||||
border: Border.all(color: theme.colorScheme.border),
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: AppColors.cardShadowLight,
|
||||
),
|
||||
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
Icon(icon, size: 17, color: theme.colorScheme.mutedForeground),
|
||||
Icon(icon, size: 15, color: AppColors.primary),
|
||||
const SizedBox(width: 4),
|
||||
Text(label, style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: theme.colorScheme.mutedForeground)),
|
||||
Text(label, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: AppColors.primary)),
|
||||
]),
|
||||
),
|
||||
);
|
||||
@@ -157,122 +157,114 @@ class _HomePageState extends ConsumerState<HomePage> {
|
||||
);
|
||||
}
|
||||
|
||||
// ═══════ 底部区域 ═══════
|
||||
Widget _buildBottomBar(BuildContext context, ActiveAgent? selectedAgent, ShadThemeData theme) {
|
||||
// ═══════ 底部输入区 ═══════
|
||||
Widget _buildBottomBar(BuildContext context) {
|
||||
return Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.only(top: AppTheme.sSm, bottom: 6),
|
||||
child: _buildAgentBar(selectedAgent, theme),
|
||||
),
|
||||
if (_pickedImagePath != null) _buildImagePreview(theme),
|
||||
_buildCompactInputBar(theme),
|
||||
Container(padding: const EdgeInsets.only(top: 4, bottom: 4), child: _buildAgentBar()),
|
||||
if (_pickedImagePath != null) _buildImagePreview(),
|
||||
_buildInputBar(),
|
||||
]);
|
||||
}
|
||||
|
||||
Widget _buildImagePreview(ShadThemeData theme) {
|
||||
Widget _buildImagePreview() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(12, 8, 12, 4),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.card,
|
||||
border: Border(top: BorderSide(color: theme.colorScheme.border)),
|
||||
),
|
||||
color: AppColors.cardInner,
|
||||
child: Row(children: [
|
||||
Stack(children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(AppTheme.rXs),
|
||||
child: Image.file(File(_pickedImagePath!), width: 60, height: 60, fit: BoxFit.cover),
|
||||
),
|
||||
Positioned(top: -4, right: -4, child: GestureDetector(
|
||||
onTap: () => setState(() => _pickedImagePath = null),
|
||||
child: Container(
|
||||
width: 20, height: 20,
|
||||
decoration: const BoxDecoration(color: AppTheme.text, shape: BoxShape.circle),
|
||||
child: const Icon(Icons.close, size: 17, color: Colors.white),
|
||||
),
|
||||
)),
|
||||
]),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.file(File(_pickedImagePath!), width: 48, height: 48, fit: BoxFit.cover),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
const Text('点击发送图片', style: TextStyle(fontSize: 14, color: AppColors.textSecondary)),
|
||||
const Spacer(),
|
||||
Text('点击发送上传图片', style: TextStyle(fontSize: 15, color: theme.colorScheme.mutedForeground)),
|
||||
GestureDetector(
|
||||
onTap: () => setState(() => _pickedImagePath = null),
|
||||
child: const Icon(Icons.close, size: 20, color: AppColors.textHint),
|
||||
),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCompactInputBar(ShadThemeData theme) {
|
||||
Widget _buildInputBar() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: AppTheme.sSm, vertical: AppTheme.sSm),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.card,
|
||||
border: Border(top: BorderSide(color: theme.colorScheme.border)),
|
||||
),
|
||||
padding: const EdgeInsets.fromLTRB(12, 8, 12, 10),
|
||||
child: Row(crossAxisAlignment: CrossAxisAlignment.end, children: [
|
||||
ShadButton.ghost(
|
||||
size: ShadButtonSize.sm,
|
||||
leading: Icon(LucideIcons.paperclip, size: 25, color: theme.colorScheme.mutedForeground),
|
||||
onPressed: () => _showAttachmentPicker(context, theme),
|
||||
),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _textCtrl,
|
||||
focusNode: _focusNode,
|
||||
style: TextStyle(fontSize: 18, color: theme.colorScheme.foreground),
|
||||
maxLines: null,
|
||||
textInputAction: TextInputAction.newline,
|
||||
decoration: InputDecoration(
|
||||
hintText: '输入你想说的...',
|
||||
hintStyle: TextStyle(fontSize: 18, color: theme.colorScheme.mutedForeground),
|
||||
filled: true,
|
||||
fillColor: theme.colorScheme.muted,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(AppTheme.rXl),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(AppTheme.rXl),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(AppTheme.rXl),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
),
|
||||
onSubmitted: (_) => _sendMessage(),
|
||||
GestureDetector(
|
||||
onTap: () => _showAttachmentPicker(context),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.only(bottom: 10),
|
||||
child: Icon(LucideIcons.paperclip, size: 24, color: AppColors.textSecondary),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
ShadButton(
|
||||
size: ShadButtonSize.sm,
|
||||
leading: Icon(LucideIcons.send, size: 21, color: theme.colorScheme.primaryForeground),
|
||||
onPressed: _sendMessage,
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxHeight: 120),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: TextField(
|
||||
controller: _textCtrl,
|
||||
focusNode: _focusNode,
|
||||
style: const TextStyle(fontSize: 15, color: AppColors.textPrimary),
|
||||
maxLines: null,
|
||||
textInputAction: TextInputAction.newline,
|
||||
decoration: const InputDecoration(
|
||||
hintText: '输入你想说的...',
|
||||
hintStyle: TextStyle(fontSize: 15, color: AppColors.textHint),
|
||||
filled: true, fillColor: Colors.white,
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(20)), borderSide: BorderSide.none),
|
||||
enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(20)), borderSide: BorderSide.none),
|
||||
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(20)), borderSide: BorderSide.none),
|
||||
),
|
||||
onSubmitted: (_) => _sendMessage(),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
GestureDetector(
|
||||
onTap: _sendMessage,
|
||||
child: Container(
|
||||
width: 40, height: 40,
|
||||
margin: EdgeInsets.only(bottom: _pickedImagePath != null ? 0 : 4),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primary,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: const Icon(LucideIcons.send, size: 20, color: Colors.white),
|
||||
),
|
||||
),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
void _showAttachmentPicker(BuildContext context, ShadThemeData theme) {
|
||||
void _showAttachmentPicker(BuildContext context) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: theme.colorScheme.card,
|
||||
backgroundColor: Colors.white,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(AppTheme.rXl)),
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
builder: (ctx) => SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: AppTheme.sMd),
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Wrap(children: [
|
||||
ListTile(
|
||||
leading: Icon(LucideIcons.camera, color: theme.colorScheme.foreground),
|
||||
title: Text('拍照', style: TextStyle(color: theme.colorScheme.foreground)),
|
||||
leading: const Icon(Icons.camera_alt_outlined, color: AppColors.primary),
|
||||
title: const Text('拍照'),
|
||||
onTap: () { Navigator.pop(ctx); _pickImage(ImageSource.camera); },
|
||||
),
|
||||
ListTile(
|
||||
leading: Icon(LucideIcons.image, color: theme.colorScheme.foreground),
|
||||
title: Text('从相册选', style: TextStyle(color: theme.colorScheme.foreground)),
|
||||
leading: const Icon(Icons.photo_library_outlined, color: AppColors.primary),
|
||||
title: const Text('从相册选'),
|
||||
onTap: () { Navigator.pop(ctx); _pickImage(ImageSource.gallery); },
|
||||
),
|
||||
ListTile(
|
||||
leading: Icon(LucideIcons.file, color: theme.colorScheme.foreground),
|
||||
title: Text('传文件', style: TextStyle(color: theme.colorScheme.foreground)),
|
||||
leading: const Icon(Icons.file_open_outlined, color: AppColors.primary),
|
||||
title: const Text('传文件'),
|
||||
onTap: () async {
|
||||
Navigator.pop(ctx);
|
||||
final result = await FilePicker.platform.pickFiles();
|
||||
@@ -289,8 +281,7 @@ class _HomePageState extends ConsumerState<HomePage> {
|
||||
}
|
||||
|
||||
Future<void> _pickImage(ImageSource source) async {
|
||||
final picker = ImagePicker();
|
||||
final picked = await picker.pickImage(source: source, imageQuality: 85);
|
||||
final picked = await ImagePicker().pickImage(source: source, imageQuality: 85);
|
||||
if (picked != null) {
|
||||
final token = await ref.read(apiClientProvider).accessToken;
|
||||
if (token == null) return;
|
||||
@@ -299,8 +290,7 @@ class _HomePageState extends ConsumerState<HomePage> {
|
||||
}
|
||||
|
||||
Future<void> _pickFoodImage(ImageSource source) async {
|
||||
final picker = ImagePicker();
|
||||
final picked = await picker.pickImage(source: source, imageQuality: 80, maxWidth: 1024, maxHeight: 1024);
|
||||
final picked = await ImagePicker().pickImage(source: source, imageQuality: 80, maxWidth: 1024, maxHeight: 1024);
|
||||
if (picked != null && mounted) {
|
||||
ref.read(dietProvider.notifier).reset();
|
||||
ref.read(dietProvider.notifier).setImage(picked.path);
|
||||
|
||||
@@ -110,14 +110,15 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
constraints: BoxConstraints(maxWidth: screenWidth * 0.95),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.cardBackground,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: Color(0xFFE2E8F0), width: 2),
|
||||
boxShadow: AppColors.cardShadow,
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// ── 标题区域(清爽蓝紫渐变背景) ──
|
||||
// ── 标题区域 ──
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.fromLTRB(20, 22, 20, 20),
|
||||
@@ -247,6 +248,15 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
),
|
||||
],
|
||||
|
||||
// ── 医生选择区(问诊专用)──
|
||||
if (agent == ActiveAgent.consultation) ...[
|
||||
const SizedBox(height: 14),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: _buildDoctorCards(ref),
|
||||
),
|
||||
],
|
||||
|
||||
// ── 快捷操作按钮区 ──
|
||||
if (actions.isNotEmpty) ...[
|
||||
const SizedBox(height: 14),
|
||||
@@ -550,14 +560,15 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
constraints: BoxConstraints(maxWidth: screenWidth * 0.95),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.cardBackground,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: Color(0xFFE2E8F0), width: 2),
|
||||
boxShadow: AppColors.cardShadow,
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// ── 标题区域(清爽蓝紫渐变背景) ──
|
||||
// ── 标题区域 ──
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.fromLTRB(20, 20, 20, 18),
|
||||
@@ -758,10 +769,7 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
msg.confirmed = true;
|
||||
if (isMedication) ref.invalidate(medicationListProvider);
|
||||
if (isHealth) ref.invalidate(latestHealthProvider);
|
||||
ref.read(chatProvider.notifier).markNeedsRebuild();
|
||||
ref.read(chatProvider.notifier).confirmMessage(msg.id);
|
||||
},
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Row(
|
||||
@@ -816,8 +824,7 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
else
|
||||
InkWell(
|
||||
onTap: () {
|
||||
msg.metadata?['_editingField'] = field.label;
|
||||
ref.read(chatProvider.notifier).markNeedsRebuild();
|
||||
ref.read(chatProvider.notifier).startEditingField(msg.id, field.label);
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
@@ -854,14 +861,10 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
border: InputBorder.none,
|
||||
),
|
||||
onSubmitted: (value) {
|
||||
msg.metadata?[field.label] = value;
|
||||
msg.metadata?['_editingField'] = null;
|
||||
ref.read(chatProvider.notifier).markNeedsRebuild();
|
||||
ref.read(chatProvider.notifier).finishEditingField(msg.id, field.label, value);
|
||||
},
|
||||
onTapOutside: (_) {
|
||||
msg.metadata?[field.label] = controller.text;
|
||||
msg.metadata?['_editingField'] = null;
|
||||
ref.read(chatProvider.notifier).markNeedsRebuild();
|
||||
ref.read(chatProvider.notifier).finishEditingField(msg.id, field.label, controller.text);
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -1057,6 +1060,7 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
.replaceAll(RegExp(r'^[\-\*]\s+', multiLine: true), '')
|
||||
.replaceAll(RegExp(r'`(.+?)`'), r'$1')
|
||||
.replaceAll(RegExp(r'\[([^\]]+)\]\([^)]+\)'), r'$1')
|
||||
.replaceAll('\$1', '') // AI 偶尔输出 $1 占位符
|
||||
.replaceAll(RegExp(r'\n{3,}'), '\n\n')
|
||||
.trim();
|
||||
}
|
||||
@@ -1328,20 +1332,35 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [BoxShadow(color: AppTheme.primary.withAlpha(10), blurRadius: 8, offset: const Offset(0, 2))],
|
||||
border: Border.all(color: Color(0xFFE2E8F0), width: 2),
|
||||
boxShadow: AppColors.cardShadow,
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Row(children: [
|
||||
Icon(Icons.today, size: 21, color: AppTheme.primary),
|
||||
const SizedBox(width: 8),
|
||||
const Text('今日任务', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: AppColors.textPrimary)),
|
||||
]),
|
||||
const SizedBox(height: 10),
|
||||
...tasks,
|
||||
Container(
|
||||
width: double.infinity, padding: const EdgeInsets.fromLTRB(16, 12, 16, 10),
|
||||
decoration: const BoxDecoration(gradient: LinearGradient(colors: [Color(0xFFF0ECFF), Color(0xFFEBF4FF)])),
|
||||
child: Row(children: [
|
||||
Container(width: 28, height: 28, decoration: BoxDecoration(color: Colors.white.withOpacity(0.6), borderRadius: BorderRadius.circular(8)),
|
||||
child: const Icon(Icons.health_and_safety, size: 16, color: AppColors.primary)),
|
||||
const SizedBox(width: 8),
|
||||
const Text('今日健康', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: AppColors.textPrimary)),
|
||||
const Spacer(),
|
||||
Text('${now.month}月${now.day}日', style: const TextStyle(fontSize: 13, color: AppColors.textSecondary)),
|
||||
]),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(14),
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
if (tasks.isEmpty)
|
||||
_taskRow(context, Icons.check_circle, '今日暂无待办', status: 'done')
|
||||
else
|
||||
...tasks,
|
||||
]),
|
||||
),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../core/app_colors.dart';
|
||||
import '../../core/app_theme.dart';
|
||||
import '../../core/navigation_provider.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../providers/auth_provider.dart';
|
||||
import '../../providers/data_providers.dart';
|
||||
@@ -29,13 +31,13 @@ class _MedicationCheckInPageState extends ConsumerState<MedicationCheckInPage> {
|
||||
ref.invalidate(medicationReminderProvider);
|
||||
ref.invalidate(medicationListProvider);
|
||||
_load();
|
||||
} catch (_) {}
|
||||
} catch (e) { debugPrint('[MedCheckIn] 打卡失败: $e'); }
|
||||
}
|
||||
|
||||
@override Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppTheme.bg,
|
||||
appBar: AppBar(title: const Text('服药打卡'), centerTitle: true),
|
||||
appBar: AppBar(leading: IconButton(icon: const Icon(Icons.arrow_back), onPressed: () => popRoute(ref)), title: const Text('服药打卡'), centerTitle: true),
|
||||
body: FutureBuilder<List<Map<String, dynamic>>>(
|
||||
future: _future,
|
||||
builder: (ctx, snap) {
|
||||
@@ -46,9 +48,9 @@ class _MedicationCheckInPageState extends ConsumerState<MedicationCheckInPage> {
|
||||
if (reminders.isEmpty) {
|
||||
return Center(
|
||||
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: AppTheme.textHint),
|
||||
const SizedBox(height: 12),
|
||||
const Text('今天所有用药已打卡', style: TextStyle(fontSize: 19, color: Color(0xFF999999))),
|
||||
const Text('今天所有用药已打卡', style: TextStyle(fontSize: 19, color: AppColors.textSecondary)),
|
||||
]),
|
||||
);
|
||||
}
|
||||
@@ -78,21 +80,21 @@ class _MedicationCheckInPageState extends ConsumerState<MedicationCheckInPage> {
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [BoxShadow(color: AppTheme.primary.withAlpha(10), blurRadius: 8, offset: const Offset(0, 2))],
|
||||
color: AppTheme.surface,
|
||||
borderRadius: BorderRadius.circular(AppTheme.rLg),
|
||||
boxShadow: [AppTheme.shadowCard],
|
||||
),
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Row(children: [
|
||||
Container(
|
||||
width: 40, height: 40,
|
||||
decoration: BoxDecoration(color: const Color(0xFFFFF3E0), borderRadius: BorderRadius.circular(10)),
|
||||
decoration: BoxDecoration(color: AppColors.warningLight, borderRadius: BorderRadius.circular(10)),
|
||||
child: const Center(child: Text('💊', style: TextStyle(fontSize: 23))),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text(medName, style: const TextStyle(fontSize: 19, fontWeight: FontWeight.w700)),
|
||||
if (dosage.isNotEmpty) Text(dosage, style: const TextStyle(fontSize: 16, color: Color(0xFF888888))),
|
||||
if (dosage.isNotEmpty) Text(dosage, style: const TextStyle(fontSize: 16, color: AppColors.textSecondary)),
|
||||
]),
|
||||
]),
|
||||
const SizedBox(height: 14),
|
||||
@@ -107,12 +109,12 @@ class _MedicationCheckInPageState extends ConsumerState<MedicationCheckInPage> {
|
||||
Icon(
|
||||
isTaken ? Icons.check_circle : Icons.radio_button_unchecked,
|
||||
size: 23,
|
||||
color: isTaken ? AppTheme.success : const Color(0xFFCCCCCC),
|
||||
color: isTaken ? AppTheme.success : AppTheme.border,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(time, style: TextStyle(
|
||||
fontSize: 18, fontWeight: FontWeight.w500,
|
||||
color: isTaken ? const Color(0xFF999999) : const Color(0xFF333333),
|
||||
color: isTaken ? AppTheme.textSub : AppTheme.text,
|
||||
)),
|
||||
const Spacer(),
|
||||
GestureDetector(
|
||||
@@ -120,12 +122,12 @@ class _MedicationCheckInPageState extends ConsumerState<MedicationCheckInPage> {
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: isTaken ? const Color(0xFFDCFCE7) : AppTheme.primary,
|
||||
color: isTaken ? AppColors.successLight : AppTheme.primary,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(isTaken ? '已打卡' : '打卡',
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600,
|
||||
color: isTaken ? AppTheme.success : Colors.white)),
|
||||
color: isTaken ? AppTheme.success : AppColors.textOnGradient)),
|
||||
),
|
||||
),
|
||||
]),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../core/app_colors.dart';
|
||||
import '../../core/app_theme.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../core/navigation_provider.dart';
|
||||
@@ -57,7 +58,7 @@ class _MedicationEditPageState extends ConsumerState<MedicationEditPage> {
|
||||
try {
|
||||
if (widget.id != null) { await srv.update(widget.id!, data); } else { await srv.create(data); }
|
||||
ref.invalidate(medicationListProvider); ref.invalidate(medicationReminderProvider);
|
||||
if (mounted) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('已保存'), backgroundColor: AppTheme.success)); popRoute(ref); }
|
||||
if (mounted) { popRoute(ref); }
|
||||
} catch (_) { if (mounted) ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('保存失败'), backgroundColor: AppTheme.error)); }
|
||||
if (mounted) setState(() => _loading = false);
|
||||
}
|
||||
@@ -113,24 +114,24 @@ class _MedicationEditPageState extends ConsumerState<MedicationEditPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _label(String text) => Text(text, style: const TextStyle(fontSize: 17, color: Color(0xFF666666)));
|
||||
Widget _label(String text) => Text(text, style: TextStyle(fontSize: 17, color: AppColors.textSecondary));
|
||||
Widget _field(String label, TextEditingController ctrl, {String? hint}) => Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
_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: 19)),
|
||||
TextField(controller: ctrl, decoration: InputDecoration(hintText: hint, filled: true, fillColor: AppTheme.surface, border: OutlineInputBorder(borderRadius: BorderRadius.circular(AppTheme.rMd), borderSide: BorderSide.none), contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12)), style: const TextStyle(fontSize: 19)),
|
||||
]);
|
||||
Widget _chip(String label, bool sel, VoidCallback onTap) => GestureDetector(onTap: onTap, child: Container(padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration(color: sel ? AppTheme.primary : AppTheme.primaryLight, borderRadius: BorderRadius.circular(16)), child: Text(label, style: TextStyle(fontSize: 16, color: sel ? Colors.white : AppTheme.primary, fontWeight: FontWeight.w500))));
|
||||
Widget _dateField(String label, DateTime val, ValueChanged<DateTime> cb) => Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
_label(label), const SizedBox(height: 6),
|
||||
GestureDetector(onTap: () async { final d = await showDatePicker(context: context, initialDate: val, firstDate: DateTime(2024), lastDate: DateTime(2030)); if (d != null) cb(d); },
|
||||
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)))),
|
||||
child: Container(width: double.infinity, padding: const EdgeInsets.all(12), decoration: BoxDecoration(color: AppTheme.surface, borderRadius: BorderRadius.circular(AppTheme.rMd)), 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: [
|
||||
_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); },
|
||||
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: 18, color: val != null ? null : Colors.grey[400])),
|
||||
child: Container(width: double.infinity, padding: const EdgeInsets.all(12), decoration: BoxDecoration(color: AppTheme.surface, borderRadius: BorderRadius.circular(AppTheme.rMd)), child: Row(children: [
|
||||
Text(val != null ? '${val.month}/${val.day}' : '不设置', style: TextStyle(fontSize: 18, color: val != null ? null : AppTheme.textHint)),
|
||||
if (val != null) const Spacer(),
|
||||
if (val != null) GestureDetector(onTap: () => cb(null), child: const Icon(Icons.close, size: 19, color: Color(0xFFBBBBBB))),
|
||||
if (val != null) GestureDetector(onTap: () => cb(null), child: const Icon(Icons.close, size: 19, color: AppColors.textHint)),
|
||||
]))),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import '../../core/navigation_provider.dart';
|
||||
import '../../providers/data_providers.dart';
|
||||
import '../../widgets/app_empty_state.dart';
|
||||
import '../../widgets/app_tab_chip.dart';
|
||||
import '../../widgets/common_widgets.dart';
|
||||
|
||||
class MedicationListPage extends ConsumerStatefulWidget {
|
||||
const MedicationListPage({super.key});
|
||||
@@ -21,16 +22,17 @@ class _MedicationListPageState extends ConsumerState<MedicationListPage> {
|
||||
|
||||
Future<void> _delete(String id) async {
|
||||
await ref.read(medicationServiceProvider).deleteMedication(id);
|
||||
ref.invalidate(medicationListProvider);
|
||||
_load();
|
||||
}
|
||||
|
||||
static const _tabs = [('全部', ''), ('服用中', 'active'), ('已停药', 'inactive')];
|
||||
|
||||
@override Widget build(BuildContext context) {
|
||||
final theme = ShadTheme.of(context);
|
||||
return Scaffold(
|
||||
backgroundColor: theme.colorScheme.background,
|
||||
backgroundColor: AppTheme.bg,
|
||||
appBar: AppBar(
|
||||
leading: IconButton(icon: const Icon(Icons.arrow_back), onPressed: () => popRoute(ref)),
|
||||
title: const Text('用药管理'),
|
||||
actions: [
|
||||
ShadButton.ghost(
|
||||
@@ -69,64 +71,48 @@ class _MedicationListPageState extends ConsumerState<MedicationListPage> {
|
||||
final times = (m['timeOfDay'] as List?)?.map((t) => t.toString().substring(0, 5)).join(' ') ?? '';
|
||||
final isActive = m['isActive'] == true;
|
||||
final id = m['id']?.toString() ?? '';
|
||||
return Dismissible(
|
||||
return SwipeDeleteTile(
|
||||
key: Key(id),
|
||||
direction: DismissDirection.endToStart,
|
||||
confirmDismiss: (_) async {
|
||||
await _delete(id);
|
||||
return false;
|
||||
},
|
||||
background: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: AppTheme.sLg, vertical: 4),
|
||||
onDelete: () => _delete(id),
|
||||
onTap: () => pushRoute(ref, 'medCheckIn', params: {'id': id}),
|
||||
margin: const EdgeInsets.symmetric(horizontal: AppTheme.sLg, vertical: 4),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(AppTheme.sLg),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.destructive,
|
||||
color: AppTheme.surface,
|
||||
borderRadius: BorderRadius.circular(AppTheme.rMd),
|
||||
boxShadow: [AppTheme.shadowLight],
|
||||
),
|
||||
alignment: Alignment.centerRight,
|
||||
padding: const EdgeInsets.only(right: 20),
|
||||
child: Icon(LucideIcons.trash, color: theme.colorScheme.destructiveForeground, size: 25),
|
||||
),
|
||||
child: GestureDetector(
|
||||
onTap: () => pushRoute(ref, 'medCheckIn', params: {'id': id}),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: AppTheme.sLg, vertical: 4),
|
||||
padding: const EdgeInsets.all(AppTheme.sLg),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.card,
|
||||
borderRadius: BorderRadius.circular(AppTheme.rMd),
|
||||
boxShadow: [AppTheme.shadowLight],
|
||||
),
|
||||
child: Row(children: [
|
||||
Container(
|
||||
width: 44, height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: isActive ? AppTheme.primaryLight : theme.colorScheme.muted,
|
||||
borderRadius: BorderRadius.circular(AppTheme.rSm),
|
||||
),
|
||||
child: Icon(LucideIcons.pill, size: 25, color: isActive ? AppTheme.primary : theme.colorScheme.mutedForeground),
|
||||
child: Row(children: [
|
||||
Container(
|
||||
width: 44, height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: isActive ? AppTheme.primaryLight : AppTheme.bgSoft,
|
||||
borderRadius: BorderRadius.circular(AppTheme.rSm),
|
||||
),
|
||||
const SizedBox(width: AppTheme.sMd),
|
||||
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Row(children: [
|
||||
Text(m['name']?.toString() ?? '', style: TextStyle(fontSize: 19, fontWeight: FontWeight.w600, color: isActive ? theme.colorScheme.foreground : theme.colorScheme.mutedForeground)),
|
||||
if (!isActive) ...[
|
||||
const SizedBox(width: 6),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.muted,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text('已停', style: TextStyle(fontSize: 13, color: theme.colorScheme.mutedForeground)),
|
||||
child: Icon(LucideIcons.pill, size: 25, color: isActive ? AppTheme.primary : AppTheme.textSub),
|
||||
),
|
||||
const SizedBox(width: AppTheme.sMd),
|
||||
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Row(children: [
|
||||
Text(m['name']?.toString() ?? '', style: TextStyle(fontSize: 19, fontWeight: FontWeight.w600, color: isActive ? AppTheme.text : AppTheme.textSub)),
|
||||
if (!isActive) ...[
|
||||
const SizedBox(width: 6),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.bgSoft,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
],
|
||||
]),
|
||||
const SizedBox(height: 2),
|
||||
Text('${m['dosage'] ?? ''} $times', style: TextStyle(fontSize: 16, color: theme.colorScheme.mutedForeground)),
|
||||
])),
|
||||
Icon(LucideIcons.chevronRight, size: 21, color: theme.colorScheme.border),
|
||||
]),
|
||||
),
|
||||
child: Text('已停', style: TextStyle(fontSize: 13, color: AppTheme.textSub)),
|
||||
),
|
||||
],
|
||||
]),
|
||||
const SizedBox(height: 2),
|
||||
Text('${m['dosage'] ?? ''} $times', style: TextStyle(fontSize: 16, color: AppTheme.textSub)),
|
||||
])),
|
||||
Icon(LucideIcons.chevronRight, size: 21, color: AppTheme.border),
|
||||
]),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
import '../../core/app_colors.dart';
|
||||
import '../../core/app_theme.dart';
|
||||
import '../../core/navigation_provider.dart';
|
||||
import '../../providers/auth_provider.dart';
|
||||
import '../../widgets/app_menu_item.dart';
|
||||
|
||||
class ProfilePage extends ConsumerWidget {
|
||||
const ProfilePage({super.key});
|
||||
@@ -12,100 +11,178 @@ class ProfilePage extends ConsumerWidget {
|
||||
@override Widget build(BuildContext context, WidgetRef ref) {
|
||||
final auth = ref.watch(authProvider);
|
||||
final user = auth.user;
|
||||
final theme = ShadTheme.of(context);
|
||||
final name = user?.name?.isNotEmpty == true ? user!.name! : '未设置昵称';
|
||||
final phone = user?.phone ?? '';
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: theme.colorScheme.background,
|
||||
body: SafeArea(child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.only(bottom: AppTheme.sXl),
|
||||
child: Column(children: [
|
||||
// 用户头部卡片
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(AppTheme.rXl),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.card,
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomLeft: Radius.circular(AppTheme.rXl),
|
||||
bottomRight: Radius.circular(AppTheme.rXl),
|
||||
),
|
||||
boxShadow: [AppTheme.shadowLight],
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: () => pushRoute(ref, 'healthArchive'),
|
||||
borderRadius: BorderRadius.circular(AppTheme.rSm),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: AppTheme.sSm),
|
||||
child: Row(children: [
|
||||
Stack(children: [
|
||||
CircleAvatar(
|
||||
radius: 32,
|
||||
backgroundColor: AppTheme.primaryLight,
|
||||
backgroundImage: user?.avatarUrl != null ? NetworkImage(user!.avatarUrl!) : null,
|
||||
child: user?.avatarUrl == null ? const Icon(Icons.person, size: 36, color: AppTheme.primary) : null,
|
||||
),
|
||||
Positioned(
|
||||
right: 0, bottom: 0,
|
||||
child: Container(
|
||||
width: 20, height: 20,
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.primary,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: theme.colorScheme.card, width: 2),
|
||||
),
|
||||
child: const Icon(Icons.edit, size: 13, color: Colors.white),
|
||||
),
|
||||
),
|
||||
]),
|
||||
const SizedBox(width: AppTheme.sLg),
|
||||
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text(user?.name ?? '未设置昵称',
|
||||
style: TextStyle(fontSize: 23, fontWeight: FontWeight.bold, color: theme.colorScheme.foreground)),
|
||||
const SizedBox(height: 4),
|
||||
Text(user?.phone ?? '', style: TextStyle(fontSize: 17, color: theme.colorScheme.mutedForeground)),
|
||||
])),
|
||||
Icon(LucideIcons.chevronRight, size: 25, color: theme.colorScheme.mutedForeground),
|
||||
]),
|
||||
),
|
||||
),
|
||||
backgroundColor: AppColors.background,
|
||||
appBar: AppBar(
|
||||
backgroundColor: AppColors.cardBackground,
|
||||
title: const Text('个人信息'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => pushRoute(ref, 'healthArchive'),
|
||||
child: const Text('编辑档案', style: TextStyle(fontSize: 15, color: AppColors.primary)),
|
||||
),
|
||||
const SizedBox(height: AppTheme.sMd),
|
||||
AppMenuItem(icon: LucideIcons.folderArchive, title: '健康档案', onTap: () => pushRoute(ref, 'healthArchive')),
|
||||
AppMenuItem(icon: LucideIcons.settings, title: '设置', onTap: () => pushRoute(ref, 'settings')),
|
||||
const SizedBox(height: 40),
|
||||
// 退出登录
|
||||
GestureDetector(
|
||||
onTap: () => _logout(context, ref),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: AppTheme.rXl),
|
||||
height: 50,
|
||||
alignment: Alignment.center,
|
||||
],
|
||||
),
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(children: [
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// 头像区
|
||||
Center(
|
||||
child: Stack(children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(3),
|
||||
decoration: BoxDecoration(
|
||||
gradient: AppColors.gPurplePink,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: CircleAvatar(
|
||||
radius: 50,
|
||||
backgroundColor: AppColors.cardBackground,
|
||||
backgroundImage: user?.avatarUrl != null ? NetworkImage(user!.avatarUrl!) : null,
|
||||
child: user?.avatarUrl == null
|
||||
? const Icon(Icons.person, size: 48, color: AppColors.primary)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: 0, bottom: 0,
|
||||
child: Container(
|
||||
width: 34, height: 34,
|
||||
decoration: BoxDecoration(
|
||||
gradient: AppColors.gPurplePink,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: AppColors.cardBackground, width: 3),
|
||||
),
|
||||
child: const Icon(Icons.camera_alt, size: 17, color: AppColors.textOnGradient),
|
||||
),
|
||||
),
|
||||
]),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(name, style: const TextStyle(fontSize: 24, fontWeight: FontWeight.w700, color: AppColors.textPrimary)),
|
||||
const SizedBox(height: 6),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: theme.colorScheme.destructive.withAlpha(80)),
|
||||
color: AppColors.primaryLight,
|
||||
borderRadius: BorderRadius.circular(AppTheme.rPill),
|
||||
),
|
||||
child: Text('退出登录', style: TextStyle(fontSize: 19, color: theme.colorScheme.destructive, fontWeight: FontWeight.w500)),
|
||||
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
const Icon(Icons.phone_android, size: 16, color: AppColors.primary),
|
||||
const SizedBox(width: 6),
|
||||
Text(phone.isNotEmpty ? phone : '未绑定手机', style: const TextStyle(fontSize: 14, color: AppColors.primary)),
|
||||
]),
|
||||
),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// 基础信息卡片
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.cardBackground,
|
||||
borderRadius: BorderRadius.circular(AppTheme.rLg),
|
||||
boxShadow: [AppTheme.shadowCard],
|
||||
),
|
||||
child: Column(children: [
|
||||
_infoRow(Icons.person_outline, '姓名', name),
|
||||
const Divider(height: 24, color: AppColors.borderLight),
|
||||
_infoRow(Icons.phone_android, '手机号', phone.isNotEmpty ? phone : '未绑定'),
|
||||
const Divider(height: 24, color: AppColors.borderLight),
|
||||
_infoRow(Icons.health_and_safety, '健康档案', '查看详情', onTap: () => pushRoute(ref, 'healthArchive')),
|
||||
]),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 快捷入口
|
||||
Row(children: [
|
||||
Expanded(child: _actionCard(Icons.folder_outlined, '健康档案', () => pushRoute(ref, 'healthArchive'))),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: _actionCard(Icons.bluetooth_rounded, '蓝牙设备', () => pushRoute(ref, 'devices'))),
|
||||
]),
|
||||
const SizedBox(height: 12),
|
||||
Row(children: [
|
||||
Expanded(child: _actionCard(Icons.settings_outlined, '设置', () => pushRoute(ref, 'settings'))),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: _actionCard(Icons.logout, '退出登录', () => _logout(context, ref), isDestructive: true)),
|
||||
]),
|
||||
|
||||
const SizedBox(height: 40),
|
||||
]),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _infoRow(IconData icon, String label, String value, {VoidCallback? onTap}) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Row(children: [
|
||||
Container(
|
||||
width: 40, height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primaryLight,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(icon, size: 22, color: AppColors.primary),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text(label, style: const TextStyle(fontSize: 13, color: AppColors.textHint)),
|
||||
const SizedBox(height: 3),
|
||||
Text(value, style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w500, color: AppColors.textPrimary)),
|
||||
]),
|
||||
),
|
||||
if (onTap != null) const Icon(Icons.chevron_right, size: 20, color: AppColors.textHint),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _actionCard(IconData icon, String label, VoidCallback onTap, {bool isDestructive = false}) {
|
||||
final c = isDestructive ? AppColors.error : AppColors.primary;
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.cardBackground,
|
||||
borderRadius: BorderRadius.circular(AppTheme.rMd),
|
||||
boxShadow: [AppTheme.shadowLight],
|
||||
),
|
||||
child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||
Icon(icon, size: 20, color: c),
|
||||
const SizedBox(width: 8),
|
||||
Text(label, style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: isDestructive ? c : AppColors.textPrimary)),
|
||||
]),
|
||||
)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _logout(BuildContext context, WidgetRef ref) async {
|
||||
final theme = ShadTheme.of(context);
|
||||
final ok = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppTheme.rXl)),
|
||||
title: Text('退出登录', style: TextStyle(color: theme.colorScheme.foreground)),
|
||||
content: Text('确定退出?', style: TextStyle(color: theme.colorScheme.mutedForeground)),
|
||||
title: const Text('退出登录'),
|
||||
content: const Text('确定退出当前账号?'),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(ctx, false), child: Text('取消', style: TextStyle(color: theme.colorScheme.mutedForeground))),
|
||||
TextButton(onPressed: () => Navigator.pop(ctx, true), child: Text('确定', style: TextStyle(color: theme.colorScheme.destructive))),
|
||||
TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('取消')),
|
||||
TextButton(onPressed: () => Navigator.pop(ctx, true), child: const Text('确定', style: TextStyle(color: AppColors.error))),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (ok == true) { await ref.read(authProvider.notifier).logout(); goRoute(ref, 'login'); }
|
||||
if (ok == true) {
|
||||
await ref.read(authProvider.notifier).logout();
|
||||
goRoute(ref, 'login');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import '../core/navigation_provider.dart';
|
||||
import '../providers/auth_provider.dart';
|
||||
import '../providers/data_providers.dart';
|
||||
import '../providers/omron_device_provider.dart';
|
||||
import '../widgets/common_widgets.dart';
|
||||
|
||||
/// 饮食记录列表(左滑删除)
|
||||
class DietRecordListPage extends ConsumerStatefulWidget {
|
||||
@@ -22,7 +23,7 @@ class _DietRecordListPageState extends ConsumerState<DietRecordListPage> {
|
||||
@override Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppTheme.bg,
|
||||
appBar: AppBar(title: const Text('饮食记录')),
|
||||
appBar: AppBar(leading: IconButton(icon: const Icon(Icons.arrow_back), onPressed: () => popRoute(ref)), title: const Text('饮食记录')),
|
||||
body: FutureBuilder<List<Map<String, dynamic>>>(
|
||||
future: _future,
|
||||
builder: (ctx, snap) {
|
||||
@@ -38,7 +39,7 @@ class _DietRecordListPageState extends ConsumerState<DietRecordListPage> {
|
||||
final mealNames = {'Breakfast':'早餐','Lunch':'午餐','Dinner':'晚餐','Snack':'加餐'};
|
||||
final mealLabel = mealNames[d['mealType']?.toString()] ?? d['mealType']?.toString() ?? '';
|
||||
final mealIcons = {'Breakfast':'🌅','Lunch':'☀️','Dinner':'🌙','Snack':'🍪'};
|
||||
return _SwipeDeleteTile(
|
||||
return SwipeDeleteTile(
|
||||
key: Key(d['id']?.toString() ?? '$i'),
|
||||
onDelete: () async {
|
||||
await ref.read(dietServiceProvider).deleteRecord(d['id']?.toString() ?? '');
|
||||
@@ -46,11 +47,10 @@ class _DietRecordListPageState extends ConsumerState<DietRecordListPage> {
|
||||
},
|
||||
onTap: () => pushRoute(ref, 'dietDetail', params: {'id': d['id']?.toString() ?? ''}),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16)),
|
||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(AppTheme.rMd)),
|
||||
child: Row(children: [
|
||||
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)))),
|
||||
Container(width: 44, height: 44, decoration: BoxDecoration(color: AppColors.iconBg, borderRadius: BorderRadius.circular(AppTheme.rMd)), child: Center(child: Text(mealIcons[d['mealType']?.toString()] ?? '🍽️', style: const TextStyle(fontSize: 23)))),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Row(children: [
|
||||
@@ -59,9 +59,9 @@ class _DietRecordListPageState extends ConsumerState<DietRecordListPage> {
|
||||
Text('${d['totalCalories'] ?? 0}千卡', style: const TextStyle(fontSize: 16, color: AppTheme.primary)),
|
||||
]),
|
||||
const SizedBox(height: 4),
|
||||
Text(items.map((f) => f['name']).join(' · '), style: const TextStyle(fontSize: 16, color: Color(0xFF888888)), maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
Text(items.map((f) => f['name']).join(' · '), style: const TextStyle(fontSize: 16, color: AppColors.textSecondary), maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
])),
|
||||
const Icon(Icons.chevron_right, size: 21, color: Color(0xFFCCCCCC)),
|
||||
Icon(Icons.chevron_right, size: 21, color: AppColors.textHint),
|
||||
]),
|
||||
),
|
||||
);
|
||||
@@ -81,7 +81,7 @@ class DietRecordDetailPage extends ConsumerWidget {
|
||||
final service = ref.watch(dietServiceProvider);
|
||||
return Scaffold(
|
||||
backgroundColor: AppTheme.bg,
|
||||
appBar: AppBar(title: const Text('饮食详情')),
|
||||
appBar: AppBar(leading: IconButton(icon: const Icon(Icons.arrow_back), onPressed: () => popRoute(ref)), title: const Text('饮食详情')),
|
||||
body: FutureBuilder<List<Map<String, dynamic>>>(
|
||||
future: service.getRecords(),
|
||||
builder: (ctx, snap) {
|
||||
@@ -92,23 +92,23 @@ class DietRecordDetailPage extends ConsumerWidget {
|
||||
final totalCal = d['totalCalories'] ?? 0;
|
||||
final mealNames = {'Breakfast':'早餐','Lunch':'午餐','Dinner':'晚餐','Snack':'加餐'};
|
||||
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(width: 48, height: 48, decoration: BoxDecoration(color: AppTheme.primaryLight, borderRadius: BorderRadius.circular(14)), child: const Icon(Icons.restaurant, size: 28, color: AppTheme.primary)),
|
||||
Container(padding: const EdgeInsets.all(16), decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(AppTheme.rMd)), child: Row(children: [
|
||||
Container(width: 48, height: 48, decoration: BoxDecoration(color: AppColors.iconBg, borderRadius: BorderRadius.circular(AppTheme.rMd)), child: const Icon(Icons.restaurant, size: 28, color: AppTheme.primary)),
|
||||
const SizedBox(width: 14),
|
||||
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text(mealNames[d['mealType']?.toString()] ?? '', style: const TextStyle(fontSize: 21, fontWeight: FontWeight.w700)),
|
||||
Text('$totalCal 千卡 · ${d['recordedAt'] ?? ''}', style: const TextStyle(fontSize: 16, color: Color(0xFF999999))),
|
||||
Text('$totalCal 千卡 · ${d['recordedAt'] ?? ''}', style: const TextStyle(fontSize: 16, color: AppColors.textHint)),
|
||||
]),
|
||||
])),
|
||||
const SizedBox(height: 16),
|
||||
...items.map((f) => Container(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(14)),
|
||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(AppTheme.rMd)),
|
||||
child: Row(children: [
|
||||
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
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: 16, color: Color(0xFF888888))),
|
||||
if ((f['portion']?.toString() ?? '').isNotEmpty) Text(f['portion']!.toString(), style: const TextStyle(fontSize: 16, color: AppColors.textSecondary)),
|
||||
])),
|
||||
Text('${f['calories'] ?? 0} kcal', style: const TextStyle(fontSize: 18, color: AppTheme.primary, fontWeight: FontWeight.w600)),
|
||||
]),
|
||||
@@ -138,13 +138,14 @@ class _ExercisePlanPageState extends ConsumerState<ExercisePlanPage> {
|
||||
|
||||
Future<void> _deletePlan(String id) async {
|
||||
await ref.read(exerciseServiceProvider).deletePlan(id);
|
||||
ref.invalidate(currentExercisePlanProvider);
|
||||
_load();
|
||||
}
|
||||
|
||||
@override Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppTheme.bg,
|
||||
appBar: AppBar(title: const Text('运动计划')),
|
||||
appBar: AppBar(leading: IconButton(icon: const Icon(Icons.arrow_back), onPressed: () => popRoute(ref)), title: const Text('运动计划')),
|
||||
floatingActionButton: FloatingActionButton(onPressed: () { pushRoute(ref, 'exerciseCreate'); }, backgroundColor: AppTheme.primary, child: const Icon(Icons.add)),
|
||||
body: FutureBuilder<List<Map<String, dynamic>>>(
|
||||
future: _future,
|
||||
@@ -157,51 +158,44 @@ class _ExercisePlanPageState extends ConsumerState<ExercisePlanPage> {
|
||||
if (items.isEmpty) return const SizedBox.shrink();
|
||||
final total = items.length;
|
||||
final done = items.where((it) => it['isCompleted'] == true).length;
|
||||
// 基于weekStartDate推算今天对应的条目索引
|
||||
final weekStart = p['weekStartDate']?.toString() ?? '';
|
||||
int? todayIdx;
|
||||
if (weekStart.isNotEmpty) {
|
||||
final ws = DateTime.tryParse(weekStart);
|
||||
if (ws != null) {
|
||||
final diff = DateTime.now().difference(ws).inDays;
|
||||
if (diff >= 0 && diff < items.length) todayIdx = diff;
|
||||
}
|
||||
}
|
||||
final todayItem = todayIdx != null ? items[todayIdx] : null;
|
||||
final todayDone = todayItem?['isCompleted'] == true;
|
||||
// 用 DayOfWeek 匹配今天(C#: 0=Sun, 6=Sat; Dart weekday%7 对齐)
|
||||
final todayCsDow = DateTime.now().weekday % 7;
|
||||
final todayItem = items.cast<Map<String, dynamic>>().firstWhere(
|
||||
(it) => it['dayOfWeek'] == todayCsDow,
|
||||
orElse: () => <String, dynamic>{},
|
||||
);
|
||||
final hasTodayItem = todayItem.isNotEmpty;
|
||||
final todayDone = todayItem['isCompleted'] == true;
|
||||
final exerciseName = items.isNotEmpty ? (items.first['exerciseType']?.toString() ?? '运动') : '运动';
|
||||
final startDate = weekStart;
|
||||
final endIdx = items.length - 1;
|
||||
final endDay = items.isNotEmpty ? items.last['dayOfWeek'] as int? : 6;
|
||||
|
||||
return _SwipeDeleteTile(
|
||||
return SwipeDeleteTile(
|
||||
key: Key(p['id']?.toString() ?? '$i'),
|
||||
onDelete: () => _deletePlan(p['id']?.toString() ?? ''),
|
||||
onTap: () {},
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16)),
|
||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(AppTheme.rMd)),
|
||||
child: Row(children: [
|
||||
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)),
|
||||
Container(width: 44, height: 44, decoration: BoxDecoration(color: AppColors.iconBg, borderRadius: BorderRadius.circular(AppTheme.rMd)), child: const Icon(Icons.directions_run, size: 25, color: AppTheme.primary)),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text(exerciseName, style: const TextStyle(fontSize: 19, fontWeight: FontWeight.w600)),
|
||||
const SizedBox(height: 2),
|
||||
Text('$startDate 起 · ${items.first['durationMinutes'] ?? 0}分钟/天', style: const TextStyle(fontSize: 16, color: Color(0xFF888888))),
|
||||
Text('$weekStart 起 · ${items.first['durationMinutes'] ?? 0}分钟/天', style: const TextStyle(fontSize: 16, color: AppColors.textSecondary)),
|
||||
const SizedBox(height: 6),
|
||||
Text('$done/$total 天已完成', style: const TextStyle(fontSize: 15, color: AppTheme.primary)),
|
||||
])),
|
||||
GestureDetector(
|
||||
onTap: todayItem != null ? () => _checkIn(p['id']?.toString() ?? '', todayItem['id']?.toString() ?? '') : null,
|
||||
onTap: hasTodayItem ? () => _checkIn(p['id']?.toString() ?? '', todayItem['id']?.toString() ?? '') : null,
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
width: 40, height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: todayDone ? const Color(0xFFDCFCE7) : (todayItem != null ? AppTheme.primaryLight : const Color(0xFFF5F5F5)),
|
||||
color: todayDone ? AppColors.successLight : (hasTodayItem ? AppTheme.primaryLight : AppColors.cardInner),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(todayDone ? Icons.check_circle : Icons.check_circle_outline, size: 28, color: todayDone ? AppTheme.success : const Color(0xFFBBBBBB)),
|
||||
child: Icon(todayDone ? Icons.check_circle : Icons.check_circle_outline, size: 28, color: todayDone ? AppTheme.success : AppColors.textHint),
|
||||
),
|
||||
),
|
||||
]),
|
||||
@@ -237,13 +231,13 @@ class _ExercisePlanCreatePageState extends ConsumerState<ExercisePlanCreatePage>
|
||||
'endDate': '${_end.year}-${_end.month.toString().padLeft(2,'0')}-${_end.day.toString().padLeft(2,'0')}',
|
||||
});
|
||||
ref.invalidate(currentExercisePlanProvider);
|
||||
if (mounted) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('计划已创建'), backgroundColor: Color(0xFF43A047))); popRoute(ref); }
|
||||
if (mounted) { popRoute(ref); }
|
||||
}
|
||||
|
||||
@override Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppTheme.bg,
|
||||
appBar: AppBar(title: const Text('新建计划')),
|
||||
appBar: AppBar(leading: IconButton(icon: const Icon(Icons.arrow_back), onPressed: () => popRoute(ref)), title: const Text('新建计划')),
|
||||
body: ListView(padding: const EdgeInsets.all(16), children: [
|
||||
_field('运动名称', _nameCtrl, hint: '如: 散步、慢跑、太极'),
|
||||
const SizedBox(height: 16),
|
||||
@@ -253,22 +247,22 @@ class _ExercisePlanCreatePageState extends ConsumerState<ExercisePlanCreatePage>
|
||||
const SizedBox(height: 16),
|
||||
_dateField('结束日期', _end, (d) => setState(() => _end = d)),
|
||||
const SizedBox(height: 32),
|
||||
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)))),
|
||||
SizedBox(width: double.infinity, child: ElevatedButton(onPressed: _save, style: ElevatedButton.styleFrom(backgroundColor: AppTheme.primary, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppTheme.rMd)), padding: const EdgeInsets.symmetric(vertical: 14)), child: const Text('保存计划', style: TextStyle(fontSize: 19)))),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _field(String label, TextEditingController ctrl, {String? hint, bool number = false}) {
|
||||
return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text(label, style: const TextStyle(fontSize: 17, color: Color(0xFF666666))),
|
||||
Text(label, style: TextStyle(fontSize: 17, color: AppColors.textSecondary)),
|
||||
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: 19)),
|
||||
TextField(controller: ctrl, keyboardType: number ? TextInputType.number : null, decoration: InputDecoration(hintText: hint, filled: true, fillColor: AppColors.backgroundSoft, border: OutlineInputBorder(borderRadius: BorderRadius.circular(AppTheme.rMd), borderSide: BorderSide.none)), style: const TextStyle(fontSize: 19)),
|
||||
]);
|
||||
}
|
||||
|
||||
Widget _dateField(String label, DateTime val, ValueChanged<DateTime> onChanged) {
|
||||
return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text(label, style: const TextStyle(fontSize: 17, color: Color(0xFF666666))),
|
||||
Text(label, style: const TextStyle(fontSize: 17, color: AppColors.textSecondary)),
|
||||
const SizedBox(height: 6),
|
||||
GestureDetector(
|
||||
onTap: () async { final d = await showDatePicker(context: context, initialDate: val, firstDate: DateTime(2024), lastDate: DateTime(2030)); if (d != null) onChanged(d); },
|
||||
@@ -294,7 +288,7 @@ class _FollowUpListPageState extends ConsumerState<FollowUpListPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('复查随访'), centerTitle: true),
|
||||
appBar: AppBar(leading: IconButton(icon: const Icon(Icons.arrow_back), onPressed: () => popRoute(ref)), title: const Text('复查随访'), centerTitle: true),
|
||||
body: FutureBuilder<List<Map<String, dynamic>>>(
|
||||
future: _future,
|
||||
builder: (ctx, snap) {
|
||||
@@ -305,11 +299,11 @@ class _FollowUpListPageState extends ConsumerState<FollowUpListPage> {
|
||||
if (list.isEmpty) {
|
||||
return Center(
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
Icon(Icons.event_note_outlined, size: 64, color: Colors.grey[300]),
|
||||
Icon(Icons.event_note_outlined, size: 64, color: AppColors.textHint),
|
||||
const SizedBox(height: 12),
|
||||
Text('暂无随访安排', style: Theme.of(context).textTheme.bodyMedium),
|
||||
const SizedBox(height: 4),
|
||||
const Text('医生创建随访后将显示在这里', style: TextStyle(fontSize: 16, color: Color(0xFF999999))),
|
||||
const Text('医生创建随访后将显示在这里', style: TextStyle(fontSize: 16, color: AppColors.textHint)),
|
||||
]),
|
||||
);
|
||||
}
|
||||
@@ -338,19 +332,19 @@ class _FollowUpItem extends StatelessWidget {
|
||||
|
||||
Color _statusColor(String? status) {
|
||||
switch (status) {
|
||||
case 'Upcoming': return const Color(0xFF4F6EF7);
|
||||
case 'Upcoming': return AppColors.primary;
|
||||
case 'Completed': return AppTheme.success;
|
||||
case 'Cancelled': return AppTheme.error;
|
||||
default: return const Color(0xFF999999);
|
||||
default: return AppColors.textHint;
|
||||
}
|
||||
}
|
||||
|
||||
Color _statusBg(String? status) {
|
||||
switch (status) {
|
||||
case 'Upcoming': return const Color(0xFFEDF2FF);
|
||||
case 'Completed': return const Color(0xFFDCFCE7);
|
||||
case 'Cancelled': return const Color(0xFFFFF5F5);
|
||||
default: return const Color(0xFFF5F5F5);
|
||||
case 'Upcoming': return AppColors.iconBg;
|
||||
case 'Completed': return AppColors.successLight;
|
||||
case 'Cancelled': return AppColors.errorLight;
|
||||
default: return AppColors.backgroundSoft;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -366,28 +360,28 @@ class _FollowUpItem extends StatelessWidget {
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [BoxShadow(color: AppTheme.primary.withAlpha(10), blurRadius: 4, offset: const Offset(0, 2))],
|
||||
borderRadius: BorderRadius.circular(AppTheme.rMd),
|
||||
boxShadow: [AppTheme.shadowCard],
|
||||
),
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Row(children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(color: bg, borderRadius: BorderRadius.circular(8)),
|
||||
decoration: BoxDecoration(color: bg, borderRadius: BorderRadius.circular(AppTheme.rSm)),
|
||||
child: Text(label, style: TextStyle(fontSize: 15, color: color, fontWeight: FontWeight.w500)),
|
||||
),
|
||||
const Spacer(),
|
||||
if (item['doctorName'] != null)
|
||||
Text('👨⚕️ ${item['doctorName']}', style: const TextStyle(fontSize: 15, color: Color(0xFF999999))),
|
||||
Text('👨⚕️ ${item['doctorName']}', style: const TextStyle(fontSize: 15, color: AppColors.textHint)),
|
||||
]),
|
||||
const SizedBox(height: 12),
|
||||
Text(item['title']?.toString() ?? '', style: const TextStyle(fontSize: 19, fontWeight: FontWeight.w600)),
|
||||
const SizedBox(height: 4),
|
||||
if (item['scheduledAt'] != null)
|
||||
Text(_formatDateTime(item['scheduledAt']?.toString()), style: TextStyle(fontSize: 17, color: Colors.grey[500])),
|
||||
Text(_formatDateTime(item['scheduledAt']?.toString()), style: const TextStyle(fontSize: 17, color: AppColors.textHint)),
|
||||
if ((item['notes']?.toString() ?? '').isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(item['notes']?.toString() ?? '', style: TextStyle(fontSize: 16, color: Colors.grey[600])),
|
||||
Text(item['notes']?.toString() ?? '', style: const TextStyle(fontSize: 16, color: AppColors.textSecondary)),
|
||||
],
|
||||
]),
|
||||
);
|
||||
@@ -419,6 +413,7 @@ class _HealthArchivePageState extends ConsumerState<HealthArchivePage> {
|
||||
final _dietCtrl = TextEditingController();
|
||||
final _familyCtrl = TextEditingController();
|
||||
bool _loading = true;
|
||||
bool _saving = false;
|
||||
|
||||
@override void initState() { super.initState(); _load(); }
|
||||
@override void dispose() {
|
||||
@@ -452,8 +447,9 @@ class _HealthArchivePageState extends ConsumerState<HealthArchivePage> {
|
||||
}
|
||||
|
||||
Future<void> _save() async {
|
||||
final srv = ref.read(userServiceProvider);
|
||||
setState(() => _saving = true);
|
||||
try {
|
||||
final srv = ref.read(userServiceProvider);
|
||||
await srv.updateProfile(name: _nameCtrl.text, gender: _genderCtrl.text, birthDate: _birthCtrl.text);
|
||||
await srv.updateHealthArchive({
|
||||
'diagnosis': _diagnosisCtrl.text,
|
||||
@@ -464,63 +460,145 @@ class _HealthArchivePageState extends ConsumerState<HealthArchivePage> {
|
||||
'dietRestrictions': _dietCtrl.text.split('、').where((s) => s.isNotEmpty).toList(),
|
||||
'familyHistory': _familyCtrl.text,
|
||||
});
|
||||
if (mounted) ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('已保存'), backgroundColor: Color(0xFF43A047)));
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('保存成功'), backgroundColor: AppColors.success),
|
||||
);
|
||||
}
|
||||
} catch (_) {
|
||||
if (mounted) ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('保存失败'), backgroundColor: Color(0xFFE53935)));
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('保存失败,请重试'), backgroundColor: AppColors.error),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _saving = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override Widget build(BuildContext context) {
|
||||
if (_loading) return Scaffold(appBar: AppBar(title: const Text('健康档案')), body: const Center(child: CircularProgressIndicator(color: AppTheme.primary)));
|
||||
if (_loading) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.background,
|
||||
appBar: AppBar(leading: IconButton(icon: const Icon(Icons.arrow_back), onPressed: () => popRoute(ref)), title: const Text('健康档案')),
|
||||
body: const Center(child: CircularProgressIndicator(color: AppColors.primary)),
|
||||
);
|
||||
}
|
||||
return Scaffold(
|
||||
backgroundColor: AppTheme.bg,
|
||||
appBar: AppBar(title: const Text('健康档案'), actions: [
|
||||
TextButton(onPressed: _save, child: const Text('保存', style: TextStyle(color: AppTheme.primary, fontWeight: FontWeight.w600))),
|
||||
]),
|
||||
body: ListView(padding: const EdgeInsets.all(16), children: [
|
||||
_sectionTitle('个人资料'),
|
||||
_editableCard(children: [
|
||||
_field('姓名', _nameCtrl),
|
||||
Row(children: [Expanded(child: _field('性别', _genderCtrl)), const SizedBox(width: 12), Expanded(child: _field('出生日期', _birthCtrl, hint: 'YYYY-MM-DD'))]),
|
||||
backgroundColor: AppColors.background,
|
||||
appBar: AppBar(
|
||||
backgroundColor: AppColors.cardBackground,
|
||||
title: const Text('健康档案'),
|
||||
),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
_buildSection('个人资料', Icons.person_outline, [
|
||||
_buildField('姓名', _nameCtrl, hint: '请输入姓名'),
|
||||
const SizedBox(height: 12),
|
||||
Row(children: [
|
||||
Expanded(child: _buildField('性别', _genderCtrl, hint: '男/女')),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: _buildField('出生日期', _birthCtrl, hint: '如 1970-03-15')),
|
||||
]),
|
||||
]),
|
||||
const SizedBox(height: 16),
|
||||
_buildSection('医疗信息', Icons.local_hospital_outlined, [
|
||||
_buildField('主要诊断', _diagnosisCtrl, hint: '如: 冠心病'),
|
||||
const SizedBox(height: 12),
|
||||
Row(children: [
|
||||
Expanded(child: _buildField('手术类型', _surgeryCtrl, hint: '如: PCI支架植入术')),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: _buildField('手术日期', _surgeryDateCtrl, hint: '如 2026-03-15')),
|
||||
]),
|
||||
]),
|
||||
const SizedBox(height: 16),
|
||||
_buildSection('病史与限制', Icons.warning_amber_outlined, [
|
||||
_buildField('过敏史', _allergiesCtrl, hint: '多个用、分隔,如: 青霉素、海鲜'),
|
||||
const SizedBox(height: 12),
|
||||
_buildField('慢性病史', _chronicCtrl, hint: '多个用、分隔,如: 高血压、高血脂'),
|
||||
const SizedBox(height: 12),
|
||||
_buildField('饮食限制', _dietCtrl, hint: '多个用、分隔,如: 低盐、低脂'),
|
||||
const SizedBox(height: 12),
|
||||
_buildField('家族病史', _familyCtrl, hint: '如: 父亲冠心病'),
|
||||
]),
|
||||
const SizedBox(height: 24),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 52,
|
||||
child: ElevatedButton(
|
||||
onPressed: _saving ? null : _save,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primary,
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppTheme.rMd)),
|
||||
elevation: 0,
|
||||
),
|
||||
child: Text(_saving ? '保存中...' : '保存档案', style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w600)),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSection(String title, IconData icon, List<Widget> fields) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(18),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.cardBackground,
|
||||
borderRadius: BorderRadius.circular(AppTheme.rMd),
|
||||
boxShadow: [AppTheme.shadowCard],
|
||||
),
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Row(children: [
|
||||
Container(
|
||||
width: 36, height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.iconBg,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(icon, size: 20, color: AppColors.primary),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(title, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w700, color: AppColors.textPrimary)),
|
||||
]),
|
||||
_sectionTitle('医疗信息'),
|
||||
_editableCard(children: [
|
||||
_field('主要诊断', _diagnosisCtrl, hint: '如: 冠心病'),
|
||||
Row(children: [Expanded(child: _field('手术类型', _surgeryCtrl, hint: '如: PCI支架植入术')), const SizedBox(width: 12), Expanded(child: _field('手术日期', _surgeryDateCtrl, hint: 'YYYY-MM-DD'))]),
|
||||
]),
|
||||
_sectionTitle('病史与限制'),
|
||||
_editableCard(children: [
|
||||
_field('过敏史', _allergiesCtrl, hint: '多个用、分隔 如: 青霉素、海鲜'),
|
||||
_field('慢性病史', _chronicCtrl, hint: '多个用、分隔 如: 高血压、高血脂'),
|
||||
_field('饮食限制', _dietCtrl, hint: '多个用、分隔 如: 低盐、低脂'),
|
||||
_field('家族病史', _familyCtrl, hint: '如: 父亲冠心病'),
|
||||
]),
|
||||
const SizedBox(height: 20),
|
||||
SizedBox(width: double.infinity, child: ElevatedButton(onPressed: _save, child: const Text('保存档案')),),
|
||||
const SizedBox(height: 40),
|
||||
const SizedBox(height: 16),
|
||||
...fields,
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _sectionTitle(String title) => Padding(
|
||||
padding: const EdgeInsets.only(left: 4, top: 16, bottom: 8),
|
||||
child: Text(title, style: const TextStyle(fontSize: 19, fontWeight: FontWeight.w700, color: AppTheme.primary)),
|
||||
);
|
||||
|
||||
Widget _editableCard({required List<Widget> children}) => Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
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),
|
||||
);
|
||||
|
||||
Widget _field(String label, TextEditingController ctrl, {String? hint}) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text(label, style: const TextStyle(fontSize: 16, color: Color(0xFF888888))),
|
||||
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: 18)),
|
||||
]),
|
||||
);
|
||||
Widget _buildField(String label, TextEditingController ctrl, {String? hint}) {
|
||||
return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text(label, style: const TextStyle(fontSize: 14, color: AppColors.textHint)),
|
||||
const SizedBox(height: 6),
|
||||
TextField(
|
||||
controller: ctrl,
|
||||
style: const TextStyle(fontSize: 17, color: AppColors.textPrimary),
|
||||
decoration: InputDecoration(
|
||||
hintText: hint,
|
||||
hintStyle: const TextStyle(fontSize: 15, color: AppColors.textHint),
|
||||
filled: true,
|
||||
fillColor: AppColors.backgroundSoft,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(AppTheme.rSm),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(AppTheme.rSm),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(AppTheme.rSm),
|
||||
borderSide: const BorderSide(color: AppColors.primary, width: 1.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/// 健康日历
|
||||
@@ -552,12 +630,12 @@ class _HealthCalendarPageState extends ConsumerState<HealthCalendarPage> {
|
||||
events[map['date'] as String] = List<String>.from(map['events'] ?? []);
|
||||
}
|
||||
if (mounted) setState(() => _events = events);
|
||||
} catch (_) {}
|
||||
} catch (e) { debugPrint('[Calendar] 加载日历失败: $e'); }
|
||||
}
|
||||
|
||||
@override Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('健康日历'), centerTitle: true),
|
||||
appBar: AppBar(leading: IconButton(icon: const Icon(Icons.arrow_back), onPressed: () => popRoute(ref)), title: const Text('健康日历'), centerTitle: true),
|
||||
body: Column(children: [
|
||||
_buildMonthHeader(),
|
||||
_buildWeekdayHeader(),
|
||||
@@ -591,7 +669,7 @@ class _HealthCalendarPageState extends ConsumerState<HealthCalendarPage> {
|
||||
Widget _buildWeekdayHeader() {
|
||||
const weekdays = ['日', '一', '二', '三', '四', '五', '六'];
|
||||
return Row(children: weekdays.map((day) => Expanded(
|
||||
child: Center(child: Text(day, style: TextStyle(fontSize: 17, color: Colors.grey[500]))),
|
||||
child: Center(child: Text(day, style: const TextStyle(fontSize: 17, color: AppColors.textHint))),
|
||||
)).toList());
|
||||
}
|
||||
|
||||
@@ -638,7 +716,7 @@ class _HealthCalendarPageState extends ConsumerState<HealthCalendarPage> {
|
||||
'$day',
|
||||
style: TextStyle(
|
||||
fontSize: 19,
|
||||
color: isToday ? Colors.white : Colors.black,
|
||||
color: isToday ? Colors.white : AppColors.textPrimary,
|
||||
fontWeight: isToday ? FontWeight.w600 : FontWeight.normal,
|
||||
),
|
||||
),
|
||||
@@ -669,8 +747,8 @@ class _HealthCalendarPageState extends ConsumerState<HealthCalendarPage> {
|
||||
switch (type) {
|
||||
case 'medication': return AppTheme.primary;
|
||||
case 'exercise': return AppTheme.success;
|
||||
case 'followup': return const Color(0xFFF59E0B);
|
||||
default: return Colors.grey;
|
||||
case 'followup': return AppColors.warning;
|
||||
default: return AppColors.textHint;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -678,14 +756,14 @@ class _HealthCalendarPageState extends ConsumerState<HealthCalendarPage> {
|
||||
final items = [
|
||||
{'color': AppTheme.primary, 'label': '用药提醒'},
|
||||
{'color': AppTheme.success, 'label': '运动计划'},
|
||||
{'color': const Color(0xFFF59E0B), 'label': '复查随访'},
|
||||
{'color': AppColors.warning, 'label': '复查随访'},
|
||||
];
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
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))),
|
||||
const SizedBox(width: 4),
|
||||
Text(item['label'] as String, style: TextStyle(fontSize: 15, color: Colors.grey[600])),
|
||||
Text(item['label'] as String, style: const TextStyle(fontSize: 15, color: AppColors.textSecondary)),
|
||||
const SizedBox(width: 20),
|
||||
])).toList()),
|
||||
);
|
||||
@@ -774,12 +852,12 @@ class StaticTextPage extends ConsumerWidget {
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
leading: IconButton(icon: const Icon(Icons.chevron_left), onPressed: () => popRoute(ref)),
|
||||
title: Text(titles[type] ?? '', style: const TextStyle(color: Color(0xFF1A1A1A), fontWeight: FontWeight.w600)),
|
||||
title: Text(titles[type] ?? '', style: TextStyle(color: AppColors.textPrimary, fontWeight: FontWeight.w600)),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Text(contents[type] ?? '内容加载中...', style: const TextStyle(fontSize: 17, height: 1.8, color: Color(0xFF333333))),
|
||||
child: Text(contents[type] ?? '内容加载中...', style: TextStyle(fontSize: 17, height: 1.8, color: AppColors.textPrimary)),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -808,7 +886,7 @@ class DeviceManagementPage extends ConsumerWidget {
|
||||
Container(
|
||||
width: 88, height: 88,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.iconBackground,
|
||||
color: AppColors.iconBg,
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
),
|
||||
child: Icon(Icons.bluetooth_disabled, size: 44, color: AppColors.textHint),
|
||||
@@ -826,9 +904,9 @@ class DeviceManagementPage extends ConsumerWidget {
|
||||
icon: const Icon(Icons.add, size: 22),
|
||||
label: const Text('添加设备', style: TextStyle(fontSize: 17, fontWeight: FontWeight.w600)),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.iconColor,
|
||||
backgroundColor: AppColors.primary,
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppTheme.rMd)),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -845,15 +923,15 @@ class DeviceManagementPage extends ConsumerWidget {
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.cardBackground,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
borderRadius: BorderRadius.circular(AppTheme.rMd),
|
||||
boxShadow: AppColors.cardShadow,
|
||||
),
|
||||
child: Column(children: [
|
||||
Container(
|
||||
width: 72, height: 72,
|
||||
decoration: BoxDecoration(
|
||||
gradient: AppColors.purpleBlueGradient,
|
||||
borderRadius: BorderRadius.circular(22),
|
||||
gradient: AppColors.primaryGradient,
|
||||
borderRadius: BorderRadius.circular(AppTheme.rMd),
|
||||
),
|
||||
child: const Icon(Icons.bluetooth_connected, size: 36, color: Colors.white),
|
||||
),
|
||||
@@ -874,7 +952,7 @@ class DeviceManagementPage extends ConsumerWidget {
|
||||
label: const Text('解绑', style: TextStyle(color: AppColors.error)),
|
||||
style: OutlinedButton.styleFrom(
|
||||
side: const BorderSide(color: AppColors.error),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppTheme.rMd)),
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
),
|
||||
),
|
||||
@@ -886,9 +964,9 @@ class DeviceManagementPage extends ConsumerWidget {
|
||||
icon: const Icon(Icons.swap_horiz, size: 18),
|
||||
label: const Text('更换设备'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.iconColor,
|
||||
backgroundColor: AppColors.primary,
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppTheme.rMd)),
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
),
|
||||
),
|
||||
@@ -905,7 +983,7 @@ class DeviceManagementPage extends ConsumerWidget {
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.cardBackground,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
borderRadius: BorderRadius.circular(AppTheme.rMd),
|
||||
boxShadow: AppColors.cardShadow,
|
||||
),
|
||||
child: Column(children: [
|
||||
@@ -913,7 +991,7 @@ class DeviceManagementPage extends ConsumerWidget {
|
||||
Container(
|
||||
width: 6, height: 18,
|
||||
decoration: BoxDecoration(
|
||||
gradient: AppColors.purpleBlueGradient,
|
||||
gradient: AppColors.primaryGradient,
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
),
|
||||
@@ -925,8 +1003,8 @@ class DeviceManagementPage extends ConsumerWidget {
|
||||
Container(
|
||||
width: 56, height: 56,
|
||||
decoration: BoxDecoration(
|
||||
gradient: AppColors.lightGradient,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
color: AppColors.iconBg,
|
||||
borderRadius: BorderRadius.circular(AppTheme.rMd),
|
||||
),
|
||||
child: const Text('🩺', style: TextStyle(fontSize: 28)),
|
||||
),
|
||||
@@ -938,7 +1016,7 @@ class DeviceManagementPage extends ConsumerWidget {
|
||||
const Spacer(),
|
||||
if (device.lastReading!.pulse != null)
|
||||
Column(crossAxisAlignment: CrossAxisAlignment.end, children: [
|
||||
Text('${device.lastReading!.pulse}', style: const TextStyle(fontSize: 24, fontWeight: FontWeight.w700, color: AppColors.iconColor)),
|
||||
Text('${device.lastReading!.pulse}', style: const TextStyle(fontSize: 24, fontWeight: FontWeight.w700, color: AppColors.primary)),
|
||||
const Text('bpm', style: TextStyle(fontSize: 13, color: AppColors.textHint)),
|
||||
]),
|
||||
]),
|
||||
@@ -952,12 +1030,12 @@ class DeviceManagementPage extends ConsumerWidget {
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.iconBackground,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
color: AppColors.iconBg,
|
||||
borderRadius: BorderRadius.circular(AppTheme.rMd),
|
||||
),
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Row(children: [
|
||||
Icon(Icons.lightbulb_outline, size: 18, color: AppColors.iconColor),
|
||||
Icon(Icons.lightbulb_outline, size: 18, color: AppColors.primary),
|
||||
const SizedBox(width: 8),
|
||||
const Text('使用说明', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: AppColors.textPrimary)),
|
||||
]),
|
||||
@@ -974,7 +1052,7 @@ class DeviceManagementPage extends ConsumerWidget {
|
||||
Widget _guideItem(String num, String text) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 6),
|
||||
child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text('$num.', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: AppColors.iconColor)),
|
||||
Text('$num.', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: AppColors.primary)),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(child: Text(text, style: const TextStyle(fontSize: 14, color: AppColors.textSecondary))),
|
||||
]),
|
||||
@@ -995,61 +1073,8 @@ class DeviceManagementPage extends ConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════ 自定义滑动删除组件 ═══════════════════
|
||||
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 _maxSlide = 80.0;
|
||||
static const _threshold = 40.0;
|
||||
|
||||
void _onDragUpdate(DragUpdateDetails d) {
|
||||
setState(() => _dx = (_dx + d.delta.dx).clamp(-_maxSlide, 0.0));
|
||||
}
|
||||
|
||||
void _onDragEnd(DragEndDetails d) {
|
||||
if (_dx < -_threshold) {
|
||||
setState(() => _dx = -_maxSlide);
|
||||
} else {
|
||||
setState(() => _dx = 0);
|
||||
}
|
||||
}
|
||||
|
||||
void _onDelete() {
|
||||
widget.onDelete();
|
||||
setState(() => _dx = 0);
|
||||
}
|
||||
|
||||
void _closeSwipe() => setState(() => _dx = 0);
|
||||
|
||||
@override Widget build(BuildContext context) {
|
||||
return Stack(children: [
|
||||
Positioned.fill(child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
decoration: BoxDecoration(color: AppTheme.error, borderRadius: BorderRadius.circular(16)),
|
||||
child: const Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Padding(padding: EdgeInsets.only(right: 20), child: Icon(Icons.delete_outline, color: Colors.white, size: 28)),
|
||||
),
|
||||
)),
|
||||
GestureDetector(
|
||||
onTap: _dx < 0 ? () { widget.onDelete(); setState(() => _dx = 0); } : widget.onTap,
|
||||
onHorizontalDragUpdate: _onDragUpdate,
|
||||
onHorizontalDragEnd: _onDragEnd,
|
||||
child: Transform.translate(offset: Offset(_dx, 0), child: widget.child),
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _empty(BuildContext context, String title, String subtitle) =>
|
||||
Center(child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
Icon(Icons.inbox_outlined, size: 64, color: Colors.grey[300]),
|
||||
Icon(Icons.inbox_outlined, size: 64, color: AppColors.textHint),
|
||||
const SizedBox(height: 12), Text(subtitle, style: Theme.of(context).textTheme.bodyMedium),
|
||||
]));
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../core/app_colors.dart';
|
||||
import '../../core/app_theme.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../core/navigation_provider.dart';
|
||||
@@ -74,8 +75,8 @@ class _AiAnalysisPageState extends ConsumerState<AiAnalysisPage> {
|
||||
Widget _buildHeader(ReportAnalysis analysis) {
|
||||
return Container(
|
||||
width: double.infinity, padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [BoxShadow(color: AppTheme.primary.withAlpha(10), blurRadius: 8)]),
|
||||
decoration: BoxDecoration(color: AppTheme.surface, borderRadius: BorderRadius.circular(AppTheme.rLg),
|
||||
boxShadow: [AppTheme.shadowCard]),
|
||||
child: Row(children: [
|
||||
Container(width: 48, height: 48, decoration: BoxDecoration(color: AppTheme.primaryLight, borderRadius: BorderRadius.circular(12)),
|
||||
child: const Icon(Icons.auto_awesome, size: 28, color: AppTheme.primary)),
|
||||
@@ -83,7 +84,7 @@ class _AiAnalysisPageState extends ConsumerState<AiAnalysisPage> {
|
||||
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text(analysis.reportType, style: const TextStyle(fontSize: 21, fontWeight: FontWeight.w700)),
|
||||
const SizedBox(height: 4),
|
||||
const Text('AI 预解读结果 · 待医生确认', style: TextStyle(fontSize: 16, color: Color(0xFF999999))),
|
||||
const Text('AI 预解读结果 · 待医生确认', style: TextStyle(fontSize: 16, color: AppColors.textSecondary)),
|
||||
])),
|
||||
]),
|
||||
);
|
||||
@@ -94,16 +95,16 @@ class _AiAnalysisPageState extends ConsumerState<AiAnalysisPage> {
|
||||
return Container(
|
||||
width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: isReviewed ? const Color(0xFFDCFCE7) : const Color(0xFFFFF3E0),
|
||||
color: isReviewed ? AppColors.successLight : AppColors.warningLight,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
Icon(isReviewed ? Icons.check_circle : Icons.hourglass_empty, size: 21,
|
||||
color: isReviewed ? AppTheme.success : const Color(0xFFF59E0B)),
|
||||
color: isReviewed ? AppTheme.success : AppColors.warning),
|
||||
const SizedBox(width: 8),
|
||||
Text(isReviewed ? '已通过医生审核' : '等待医生审核中',
|
||||
style: TextStyle(fontSize: 17, fontWeight: FontWeight.w500,
|
||||
color: isReviewed ? AppTheme.success : const Color(0xFFF59E0B))),
|
||||
color: isReviewed ? AppTheme.success : AppColors.warning)),
|
||||
]),
|
||||
);
|
||||
}
|
||||
@@ -111,7 +112,7 @@ class _AiAnalysisPageState extends ConsumerState<AiAnalysisPage> {
|
||||
Widget _buildIndicators(ReportAnalysis analysis) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16)),
|
||||
decoration: BoxDecoration(color: AppTheme.surface, borderRadius: BorderRadius.circular(AppTheme.rLg)),
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.fromLTRB(16, 16, 16, 8),
|
||||
@@ -128,19 +129,19 @@ class _AiAnalysisPageState extends ConsumerState<AiAnalysisPage> {
|
||||
final IconData icon;
|
||||
switch (ind.status) {
|
||||
case 'high': color = AppTheme.error; icon = Icons.arrow_upward; break;
|
||||
case 'low': color = const Color(0xFFF9A825); icon = Icons.arrow_downward; break;
|
||||
case 'low': color = AppColors.warning; icon = Icons.arrow_downward; break;
|
||||
default: color = AppTheme.success; icon = Icons.check_circle;
|
||||
}
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
decoration: const BoxDecoration(border: Border(bottom: BorderSide(color: Color(0xFFF0F0F0)))),
|
||||
decoration: BoxDecoration(border: Border(bottom: BorderSide(color: AppColors.border))),
|
||||
child: Row(children: [
|
||||
Expanded(
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text(ind.name, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w500)),
|
||||
if (ind.referenceRange != null && ind.referenceRange!.isNotEmpty)
|
||||
Text('参考值: ${ind.referenceRange}', style: const TextStyle(fontSize: 15, color: Color(0xFF999999))),
|
||||
Text('参考值: ${ind.referenceRange}', style: TextStyle(fontSize: 15, color: AppColors.textHint)),
|
||||
]),
|
||||
),
|
||||
Column(crossAxisAlignment: CrossAxisAlignment.end, children: [
|
||||
@@ -155,23 +156,23 @@ class _AiAnalysisPageState extends ConsumerState<AiAnalysisPage> {
|
||||
Widget _buildSummary(ReportAnalysis analysis) {
|
||||
return Container(
|
||||
width: double.infinity, padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(color: const Color(0xFFFEFCE8), borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: const Color(0xFFFDE68A))),
|
||||
decoration: BoxDecoration(color: AppColors.warningLight, borderRadius: BorderRadius.circular(AppTheme.rLg),
|
||||
border: Border.all(color: AppColors.warning.withAlpha(80))),
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
const Row(children: [
|
||||
Text('💡', style: TextStyle(fontSize: 21)),
|
||||
SizedBox(width: 8),
|
||||
Text('综合解读', style: TextStyle(fontSize: 19, fontWeight: FontWeight.w700, color: Color(0xFFD97706))),
|
||||
Row(children: [
|
||||
const Text('💡', style: TextStyle(fontSize: 21)),
|
||||
const SizedBox(width: 8),
|
||||
Text('综合解读', style: TextStyle(fontSize: 19, fontWeight: FontWeight.w700, color: AppColors.warning)),
|
||||
]),
|
||||
const SizedBox(height: 10),
|
||||
Text(analysis.summary.isNotEmpty ? analysis.summary : '暂无 AI 解读结果',
|
||||
style: const TextStyle(fontSize: 17, color: Color(0xFF92400E), height: 1.6)),
|
||||
style: TextStyle(fontSize: 17, color: AppColors.warning, height: 1.6)),
|
||||
const SizedBox(height: 10),
|
||||
Container(
|
||||
width: double.infinity, padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(10)),
|
||||
child: const Text('⚠️ AI 解读仅供参考,请以医生诊断为准',
|
||||
style: TextStyle(fontSize: 15, color: Color(0xFFD97706))),
|
||||
decoration: BoxDecoration(color: AppTheme.surface, borderRadius: BorderRadius.circular(AppTheme.rSm)),
|
||||
child: Text('⚠️ AI 解读仅供参考,请以医生诊断为准',
|
||||
style: TextStyle(fontSize: 15, color: AppColors.warning)),
|
||||
),
|
||||
]),
|
||||
);
|
||||
@@ -180,22 +181,22 @@ class _AiAnalysisPageState extends ConsumerState<AiAnalysisPage> {
|
||||
Widget _buildDoctorReview(ReportItem report) {
|
||||
return Container(
|
||||
width: double.infinity, padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(color: const Color(0xFFDCFCE7), borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: const Color(0xFF86EFAC))),
|
||||
decoration: BoxDecoration(color: AppColors.successLight, borderRadius: BorderRadius.circular(AppTheme.rLg),
|
||||
border: Border.all(color: AppColors.success.withAlpha(80))),
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
const Row(children: [
|
||||
Text('✅', style: TextStyle(fontSize: 21)),
|
||||
SizedBox(width: 8),
|
||||
Text('医生审核意见', style: TextStyle(fontSize: 19, fontWeight: FontWeight.w700, color: Color(0xFF2E7D32))),
|
||||
Row(children: [
|
||||
const Text('✅', style: TextStyle(fontSize: 21)),
|
||||
const SizedBox(width: 8),
|
||||
Text('医生审核意见', style: TextStyle(fontSize: 19, fontWeight: FontWeight.w700, color: AppColors.success)),
|
||||
]),
|
||||
if (report.doctorName != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text('审核医生:${report.doctorName}', style: const TextStyle(fontSize: 16, color: Color(0xFF4CAF50))),
|
||||
Text('审核医生:${report.doctorName}', style: TextStyle(fontSize: 16, color: AppColors.success)),
|
||||
],
|
||||
if (report.reviewedAt != null) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text('审核时间:${report.reviewedAt!.toLocal().toString().substring(0, 16)}',
|
||||
style: const TextStyle(fontSize: 15, color: Color(0xFF81C784))),
|
||||
style: TextStyle(fontSize: 15, color: AppColors.success.withAlpha(180))),
|
||||
],
|
||||
if (report.severity != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
@@ -205,18 +206,18 @@ class _AiAnalysisPageState extends ConsumerState<AiAnalysisPage> {
|
||||
const SizedBox(height: 10),
|
||||
Container(
|
||||
width: double.infinity, padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(10)),
|
||||
decoration: BoxDecoration(color: AppTheme.surface, borderRadius: BorderRadius.circular(AppTheme.rSm)),
|
||||
child: Text('💬 ${report.doctorComment!}',
|
||||
style: const TextStyle(fontSize: 18, color: Color(0xFF1A1A1A), height: 1.5)),
|
||||
style: TextStyle(fontSize: 18, color: AppColors.textPrimary, height: 1.5)),
|
||||
),
|
||||
],
|
||||
if (report.doctorRecommendation != null && report.doctorRecommendation!.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
width: double.infinity, padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(10)),
|
||||
decoration: BoxDecoration(color: AppTheme.surface, borderRadius: BorderRadius.circular(AppTheme.rSm)),
|
||||
child: Text('📝 ${report.doctorRecommendation!}',
|
||||
style: const TextStyle(fontSize: 17, color: Color(0xFF333333), height: 1.5)),
|
||||
style: TextStyle(fontSize: 17, color: AppColors.textPrimary, height: 1.5)),
|
||||
),
|
||||
],
|
||||
]),
|
||||
@@ -225,11 +226,11 @@ class _AiAnalysisPageState extends ConsumerState<AiAnalysisPage> {
|
||||
|
||||
Widget _severityBadge(String severity) {
|
||||
final (label, color, bg) = switch (severity) {
|
||||
'Normal' => ('🟢 正常', AppTheme.success, const Color(0xFFDCFCE7)),
|
||||
'Abnormal' => ('🟡 轻度异常', const Color(0xFFF59E0B), const Color(0xFFFFF3E0)),
|
||||
'Severe' => ('🟠 中度异常', const Color(0xFFFF7043), const Color(0xFFFEE2E2)),
|
||||
'Critical' => ('🔴 重度异常', const Color(0xFFDC2626), const Color(0xFFFEE2E2)),
|
||||
_ => (severity, Colors.grey, Colors.grey.withAlpha(30)),
|
||||
'Normal' => ('🟢 正常', AppTheme.success, AppColors.successLight),
|
||||
'Abnormal' => ('🟡 轻度异常', AppColors.warning, AppColors.warningLight),
|
||||
'Severe' => ('🟠 中度异常', AppColors.error, AppColors.errorLight),
|
||||
'Critical' => ('🔴 重度异常', AppColors.error, AppColors.errorLight),
|
||||
_ => (severity, AppColors.textHint, AppColors.borderLight),
|
||||
};
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
import '../../core/app_colors.dart';
|
||||
import '../../core/app_theme.dart';
|
||||
import '../../core/navigation_provider.dart';
|
||||
import '../../providers/auth_provider.dart';
|
||||
@@ -129,7 +130,9 @@ class ReportNotifier extends Notifier<ReportState> {
|
||||
);
|
||||
}).toList();
|
||||
state = state.copyWith(reports: reports);
|
||||
} catch (_) {}
|
||||
} catch (e) {
|
||||
debugPrint('[Report] 加载报告列表失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void fetchReportDetail(String reportId) async {
|
||||
@@ -234,6 +237,7 @@ class ReportListPage extends ConsumerWidget {
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: IconButton(icon: const Icon(Icons.arrow_back), onPressed: () => popRoute(ref)),
|
||||
title: const Text('看报告'),
|
||||
centerTitle: true,
|
||||
),
|
||||
@@ -311,7 +315,7 @@ class ReportListPage extends ConsumerWidget {
|
||||
width: 120,
|
||||
height: 120,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF0F2FF),
|
||||
color: AppColors.primaryLight,
|
||||
borderRadius: BorderRadius.circular(60),
|
||||
),
|
||||
child: const Icon(Icons.file_open, size: 48, color: AppTheme.primaryLight),
|
||||
@@ -319,7 +323,7 @@ class ReportListPage extends ConsumerWidget {
|
||||
const SizedBox(height: 20),
|
||||
const Text('暂无检查报告', style: TextStyle(fontSize: 21, fontWeight: FontWeight.w500)),
|
||||
const SizedBox(height: 8),
|
||||
const Text('点击下方按钮上传报告', style: TextStyle(fontSize: 17, color: Color(0xFF999999))),
|
||||
const Text('点击下方按钮上传报告', style: TextStyle(fontSize: 17, color: AppColors.textHint)),
|
||||
]),
|
||||
);
|
||||
}
|
||||
@@ -328,24 +332,24 @@ class ReportListPage extends ConsumerWidget {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [BoxShadow(color: AppTheme.primary.withAlpha(10), blurRadius: 4, offset: const Offset(0, 2))],
|
||||
color: AppTheme.surface,
|
||||
borderRadius: BorderRadius.circular(AppTheme.rLg),
|
||||
boxShadow: [AppTheme.shadowCard],
|
||||
),
|
||||
child: ListTile(
|
||||
leading: Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF0F2FF),
|
||||
color: AppColors.primaryLight,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: _getReportIcon(report.type),
|
||||
),
|
||||
title: Text(report.title, style: const TextStyle(fontSize: 19, fontWeight: FontWeight.w600)),
|
||||
subtitle: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text(report.type, style: TextStyle(fontSize: 17, color: Colors.grey[500])),
|
||||
Text(_formatDate(report.uploadedAt), style: TextStyle(fontSize: 15, color: Colors.grey[400])),
|
||||
Text(report.type, style: TextStyle(fontSize: 17, color: AppColors.textSecondary)),
|
||||
Text(_formatDate(report.uploadedAt), style: TextStyle(fontSize: 15, color: AppColors.textHint)),
|
||||
]),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -354,38 +358,36 @@ class ReportListPage extends ConsumerWidget {
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFDCFCE7),
|
||||
color: AppColors.successLight,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: const Text('已审核', style: TextStyle(fontSize: 13, color: Color(0xFF43A047))),
|
||||
child: const Text('已审核', style: TextStyle(fontSize: 13, color: AppColors.success)),
|
||||
)
|
||||
else if (!report.hasAnalysis)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFE3F2FD),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
color: AppTheme.primaryLight,
|
||||
borderRadius: BorderRadius.circular(AppTheme.rXs),
|
||||
),
|
||||
child: const Text('分析中', style: TextStyle(fontSize: 13, color: Color(0xFF2196F3))),
|
||||
child: Text('分析中', style: TextStyle(fontSize: 13, color: AppTheme.primary)),
|
||||
)
|
||||
else if (report.status == 'PendingDoctor')
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFFF3E0),
|
||||
color: AppColors.warningLight,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: const Text('待审核', style: TextStyle(fontSize: 13, color: Color(0xFFF59E0B))),
|
||||
child: const Text('待审核', style: TextStyle(fontSize: 13, color: AppColors.warning)),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
report.hasAnalysis
|
||||
? const Icon(Icons.check_circle, size: 23, color: Color(0xFF43A047))
|
||||
: const Icon(Icons.arrow_forward_ios, size: 21, color: Color(0xFFCCCCCC)),
|
||||
? Icon(Icons.check_circle, size: 23, color: AppColors.success)
|
||||
: Icon(Icons.arrow_forward_ios, size: 21, color: AppColors.textHint),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
pushRoute(ref, 'reportDetail', params: {'id': report.id});
|
||||
},
|
||||
onTap: () => pushRoute(ref, 'reportDetail', params: {'id': report.id}),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -487,23 +489,23 @@ class _ReportDetailPageState extends ConsumerState<ReportDetailPage> {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFDCFCE7),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: const Color(0xFF43A047).withAlpha(50)),
|
||||
color: AppColors.successLight,
|
||||
borderRadius: BorderRadius.circular(AppTheme.rLg),
|
||||
border: Border.all(color: AppColors.success.withAlpha(50)),
|
||||
),
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Row(children: [
|
||||
const Text('✅', style: TextStyle(fontSize: 23)),
|
||||
const SizedBox(width: 8),
|
||||
const Text('医生审核意见', style: TextStyle(fontSize: 19, fontWeight: FontWeight.w600, color: Color(0xFF2E7D32))),
|
||||
const Text('医生审核意见', style: TextStyle(fontSize: 19, fontWeight: FontWeight.w600, color: AppColors.success)),
|
||||
]),
|
||||
if (report.doctorName != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text('审核医生:${report.doctorName}', style: const TextStyle(fontSize: 16, color: Color(0xFF4CAF50))),
|
||||
Text('审核医生:${report.doctorName}', style: TextStyle(fontSize: 16, color: AppColors.success)),
|
||||
],
|
||||
if (report.reviewedAt != null) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text('审核时间:${report.reviewedAt!.toLocal().toString().substring(0, 19)}', style: const TextStyle(fontSize: 15, color: Color(0xFF81C784))),
|
||||
Text('审核时间:${report.reviewedAt!.toLocal().toString().substring(0, 19)}', style: TextStyle(fontSize: 15, color: AppColors.success.withAlpha(180))),
|
||||
],
|
||||
if (report.severity != null) ...[
|
||||
const SizedBox(height: 6),
|
||||
@@ -515,10 +517,10 @@ class _ReportDetailPageState extends ConsumerState<ReportDetailPage> {
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: AppTheme.surface,
|
||||
borderRadius: BorderRadius.circular(AppTheme.rMd),
|
||||
),
|
||||
child: Text('💬 ${report.doctorComment!}', style: const TextStyle(fontSize: 18, color: Color(0xFF1A1A1A), height: 1.5)),
|
||||
child: Text('💬 ${report.doctorComment!}', style: TextStyle(fontSize: 18, color: AppColors.textPrimary, height: 1.5)),
|
||||
),
|
||||
],
|
||||
if (report.doctorRecommendation != null && report.doctorRecommendation!.isNotEmpty) ...[
|
||||
@@ -527,10 +529,10 @@ class _ReportDetailPageState extends ConsumerState<ReportDetailPage> {
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: AppTheme.surface,
|
||||
borderRadius: BorderRadius.circular(AppTheme.rMd),
|
||||
),
|
||||
child: Text('📝 ${report.doctorRecommendation!}', style: const TextStyle(fontSize: 17, color: Color(0xFF333333), height: 1.5)),
|
||||
child: Text('📝 ${report.doctorRecommendation!}', style: TextStyle(fontSize: 17, color: AppColors.textPrimary, height: 1.5)),
|
||||
),
|
||||
],
|
||||
]),
|
||||
@@ -539,15 +541,15 @@ class _ReportDetailPageState extends ConsumerState<ReportDetailPage> {
|
||||
|
||||
Widget _severityBadge(String severity) {
|
||||
final (label, color, bg) = switch (severity) {
|
||||
'Normal' => ('🟢 正常', const Color(0xFF43A047), const Color(0xFFDCFCE7)),
|
||||
'Abnormal' => ('🟡 轻度异常', const Color(0xFFF59E0B), const Color(0xFFFFF3E0)),
|
||||
'Severe' => ('🟠 中度异常', const Color(0xFFEF4444), const Color(0xFFFEE2E2)),
|
||||
'Critical' => ('🔴 重度异常', const Color(0xFFDC2626), const Color(0xFFFEE2E2)),
|
||||
_ => (severity, Colors.grey, Colors.grey.withAlpha(30)),
|
||||
'Normal' => ('🟢 正常', AppColors.success, AppColors.successLight),
|
||||
'Abnormal' => ('🟡 轻度异常', AppColors.warning, AppColors.warningLight),
|
||||
'Severe' => ('🟠 中度异常', AppColors.error, AppColors.errorLight),
|
||||
'Critical' => ('🔴 重度异常', AppColors.error, AppColors.errorLight),
|
||||
_ => (severity, AppColors.textSecondary, AppColors.border),
|
||||
};
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
decoration: BoxDecoration(color: bg, borderRadius: BorderRadius.circular(6)),
|
||||
decoration: BoxDecoration(color: bg, borderRadius: BorderRadius.circular(AppTheme.rXs)),
|
||||
child: Text(label, style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: color)),
|
||||
);
|
||||
}
|
||||
@@ -556,8 +558,8 @@ class _ReportDetailPageState extends ConsumerState<ReportDetailPage> {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF0F2FF),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
color: AppColors.primaryLight,
|
||||
borderRadius: BorderRadius.circular(AppTheme.rLg),
|
||||
),
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Row(children: [
|
||||
@@ -566,7 +568,7 @@ class _ReportDetailPageState extends ConsumerState<ReportDetailPage> {
|
||||
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text(analysis.reportType, style: const TextStyle(fontSize: 21, fontWeight: FontWeight.w600)),
|
||||
const SizedBox(height: 4),
|
||||
const Text('AI 预解读结果', style: TextStyle(fontSize: 17, color: Color(0xFF666666))),
|
||||
Text('AI 预解读结果', style: TextStyle(fontSize: 17, color: AppColors.textSecondary)),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
@@ -576,9 +578,9 @@ class _ReportDetailPageState extends ConsumerState<ReportDetailPage> {
|
||||
Widget _buildAnalysisSection(ReportAnalysis analysis) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: const Color(0xFFD8DCFD), width: 1.5),
|
||||
color: AppTheme.surface,
|
||||
borderRadius: BorderRadius.circular(AppTheme.rLg),
|
||||
border: Border.all(color: AppColors.border, width: 1),
|
||||
),
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Padding(
|
||||
@@ -593,18 +595,18 @@ class _ReportDetailPageState extends ConsumerState<ReportDetailPage> {
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFFF3E0),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: const Color(0xFFFFE0B2)),
|
||||
color: AppColors.warningLight,
|
||||
borderRadius: BorderRadius.circular(AppTheme.rXs),
|
||||
border: Border.all(color: AppColors.warning.withAlpha(80)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.info_outline, size: 19, color: Color(0xFFE65100)),
|
||||
Icon(Icons.info_outline, size: 19, color: AppColors.warning),
|
||||
const SizedBox(width: 6),
|
||||
const Text(
|
||||
Text(
|
||||
'AI 预解读 · 待医生确认',
|
||||
style: TextStyle(fontSize: 16, color: Color(0xFFE65100), fontWeight: FontWeight.w500),
|
||||
style: TextStyle(fontSize: 16, color: AppColors.warning, fontWeight: FontWeight.w500),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -620,28 +622,28 @@ class _ReportDetailPageState extends ConsumerState<ReportDetailPage> {
|
||||
IconData statusIcon;
|
||||
switch (ind.status) {
|
||||
case 'high':
|
||||
statusColor = const Color(0xFFE53935);
|
||||
statusColor = AppColors.error;
|
||||
statusIcon = Icons.arrow_upward;
|
||||
break;
|
||||
case 'low':
|
||||
statusColor = const Color(0xFFF9A825);
|
||||
statusColor = AppColors.warning;
|
||||
statusIcon = Icons.arrow_downward;
|
||||
break;
|
||||
default:
|
||||
statusColor = const Color(0xFF43A047);
|
||||
statusColor = AppColors.success;
|
||||
statusIcon = Icons.check_circle;
|
||||
}
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
decoration: const BoxDecoration(border: Border(bottom: BorderSide(color: Color(0xFFF0F0F0)))),
|
||||
decoration: BoxDecoration(border: Border(bottom: BorderSide(color: AppColors.border))),
|
||||
child: Row(children: [
|
||||
Expanded(
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text(ind.name, style: const TextStyle(fontSize: 17)),
|
||||
if (ind.referenceRange != null)
|
||||
Text('参考值: ${ind.referenceRange}', style: TextStyle(fontSize: 15, color: Colors.grey[400])),
|
||||
Text('参考值: ${ind.referenceRange}', style: TextStyle(fontSize: 15, color: AppColors.textHint)),
|
||||
]),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
@@ -657,25 +659,25 @@ class _ReportDetailPageState extends ConsumerState<ReportDetailPage> {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFEF3C7),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
color: AppColors.warningLight,
|
||||
borderRadius: BorderRadius.circular(AppTheme.rLg),
|
||||
),
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Row(children: [
|
||||
const Text('💡', style: TextStyle(fontSize: 23)),
|
||||
const SizedBox(width: 8),
|
||||
const Text('综合解读', style: TextStyle(fontSize: 19, fontWeight: FontWeight.w600, color: Color(0xFFD97706))),
|
||||
Text('综合解读', style: TextStyle(fontSize: 19, fontWeight: FontWeight.w600, color: AppColors.warning)),
|
||||
]),
|
||||
const SizedBox(height: 12),
|
||||
Text(analysis.summary, style: const TextStyle(fontSize: 17, color: Color(0xFF92400E), height: 1.6)),
|
||||
Text(analysis.summary, style: TextStyle(fontSize: 17, color: AppColors.warning, height: 1.6)),
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: AppTheme.surface,
|
||||
borderRadius: BorderRadius.circular(AppTheme.rSm),
|
||||
),
|
||||
child: const Text('⚠️ AI 解读仅供参考,请以医生诊断为准', style: TextStyle(fontSize: 16, color: Color(0xFFD97706))),
|
||||
child: Text('⚠️ AI 解读仅供参考,请以医生诊断为准', style: TextStyle(fontSize: 16, color: AppColors.warning)),
|
||||
),
|
||||
]),
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../core/app_colors.dart';
|
||||
import '../../core/app_theme.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../core/navigation_provider.dart';
|
||||
@@ -55,10 +56,10 @@ class NotificationPrefsPage extends ConsumerWidget {
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios_new, color: Color(0xFF1A1A1A)),
|
||||
icon: const Icon(Icons.arrow_back_ios_new, color: AppColors.textPrimary),
|
||||
onPressed: () => popRoute(ref),
|
||||
),
|
||||
title: const Text('消息通知', style: TextStyle(fontSize: 21, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
||||
title: Text('消息通知', style: TextStyle(fontSize: 21, fontWeight: FontWeight.w600, color: AppColors.textPrimary)),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
@@ -80,8 +81,8 @@ class NotificationPrefsPage extends ConsumerWidget {
|
||||
_SectionTitle(title: '通知类型'),
|
||||
_SwitchTile(
|
||||
icon: Icons.medication_rounded,
|
||||
iconBg: const Color(0xFFFFF3E0),
|
||||
iconColor: const Color(0xFFFF9800),
|
||||
iconBg: AppColors.errorLight,
|
||||
iconColor: AppColors.error,
|
||||
title: '用药提醒',
|
||||
subtitle: '服药时间到达时提醒您',
|
||||
value: prefs['medication'] ?? true,
|
||||
@@ -89,7 +90,7 @@ class NotificationPrefsPage extends ConsumerWidget {
|
||||
),
|
||||
_SwitchTile(
|
||||
icon: Icons.warning_amber_rounded,
|
||||
iconBg: const Color(0xFFFFEBEE),
|
||||
iconBg: AppColors.errorLight,
|
||||
iconColor: AppTheme.error,
|
||||
title: '健康异常提醒',
|
||||
subtitle: '检测到数据异常时及时通知',
|
||||
@@ -98,8 +99,8 @@ class NotificationPrefsPage extends ConsumerWidget {
|
||||
),
|
||||
_SwitchTile(
|
||||
icon: Icons.event_available_rounded,
|
||||
iconBg: const Color(0xFFE8F5E9),
|
||||
iconColor: const Color(0xFF4CAF50),
|
||||
iconBg: AppColors.successLight,
|
||||
iconColor: AppColors.success,
|
||||
title: '复查日期提醒',
|
||||
subtitle: '复查日前一天提醒您预约',
|
||||
value: prefs['followUp'] ?? true,
|
||||
@@ -107,7 +108,7 @@ class NotificationPrefsPage extends ConsumerWidget {
|
||||
),
|
||||
_SwitchTile(
|
||||
icon: Icons.smart_toy_outlined,
|
||||
iconBg: const Color(0xFFF3E5F5),
|
||||
iconBg: AppColors.iconBg,
|
||||
iconColor: AppTheme.primary,
|
||||
title: 'AI 回复通知',
|
||||
subtitle: 'AI 助手回复时发送通知',
|
||||
@@ -128,13 +129,13 @@ class NotificationPrefsPage extends ConsumerWidget {
|
||||
Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 4),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12)),
|
||||
decoration: BoxDecoration(color: AppTheme.surface, borderRadius: BorderRadius.circular(AppTheme.rMd)),
|
||||
child: Row(children: [
|
||||
Expanded(child: _TimeButton(label: '开始', time: '22:00', onTap: () async {
|
||||
final picked = await showTimePicker(context: context, initialTime: const TimeOfDay(hour: 22, minute: 0));
|
||||
if (picked != null && context.mounted) ref.read(notificationPrefsProvider.notifier).setDndStart(picked);
|
||||
})),
|
||||
Padding(padding: const EdgeInsets.symmetric(horizontal: 12), child: Text('~', style: TextStyle(fontSize: 19, color: Colors.grey[400]))),
|
||||
Padding(padding: const EdgeInsets.symmetric(horizontal: 12), child: Text('~', style: TextStyle(fontSize: 19, color: AppTheme.textHint))),
|
||||
Expanded(child: _TimeButton(label: '结束', time: '08:00', onTap: () async {
|
||||
final picked = await showTimePicker(context: context, initialTime: const TimeOfDay(hour: 8, minute: 0));
|
||||
if (picked != null && context.mounted) ref.read(notificationPrefsProvider.notifier).setDndEnd(picked);
|
||||
@@ -158,7 +159,7 @@ class _SectionTitle extends StatelessWidget {
|
||||
const _SectionTitle({required this.title});
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(padding: const EdgeInsets.only(left: 4, bottom: 10), child: Text(title, style: const TextStyle(fontSize: 17, 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: AppColors.textSecondary)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,17 +185,17 @@ class _SwitchTile extends StatelessWidget {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 3),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12)),
|
||||
decoration: BoxDecoration(color: AppTheme.surface, borderRadius: BorderRadius.circular(AppTheme.rMd)),
|
||||
child: Row(children: [
|
||||
if (icon != null) ...[
|
||||
Container(width: 38, height: 38, decoration: BoxDecoration(color: iconBg, borderRadius: BorderRadius.circular(10)), child: Icon(icon, size: 23, color: iconColor)),
|
||||
const SizedBox(width: 12),
|
||||
],
|
||||
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text(title, style: const TextStyle(fontSize: 18, color: Color(0xFF1A1A1A), fontWeight: FontWeight.w500)),
|
||||
if (subtitle != null && subtitle!.isNotEmpty) Text(subtitle!, style: TextStyle(fontSize: 15, color: Colors.grey[500])),
|
||||
Text(title, style: const TextStyle(fontSize: 18, color: AppColors.textPrimary, fontWeight: FontWeight.w500)),
|
||||
if (subtitle != null && subtitle!.isNotEmpty) Text(subtitle!, style: TextStyle(fontSize: 15, color: AppTheme.textSub)),
|
||||
])),
|
||||
Switch(value: value, onChanged: onChanged, activeThumbColor: AppTheme.primary, activeTrackColor: const Color(0xFFC5BFFF)),
|
||||
Switch(value: value, onChanged: onChanged, activeThumbColor: AppTheme.primary, activeTrackColor: AppColors.primaryLight),
|
||||
]),
|
||||
);
|
||||
}
|
||||
@@ -210,8 +211,8 @@ class _TimeButton extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(onTap: onTap, child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||
decoration: BoxDecoration(border: Border.all(color: const Color(0xFFE0E0E0)), borderRadius: BorderRadius.circular(10)),
|
||||
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))]),
|
||||
decoration: BoxDecoration(border: Border.all(color: AppColors.border), borderRadius: BorderRadius.circular(AppTheme.rSm)),
|
||||
child: Column(children: [Text(label, style: TextStyle(fontSize: 14, color: AppTheme.textHint)), Text(time, style: const TextStyle(fontSize: 19, fontWeight: FontWeight.w600, color: AppTheme.primary))]),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
import '../../core/app_colors.dart';
|
||||
import '../../core/app_theme.dart';
|
||||
import '../../core/navigation_provider.dart';
|
||||
import '../../providers/auth_provider.dart';
|
||||
@@ -10,15 +11,12 @@ class SettingsPage extends ConsumerWidget {
|
||||
const SettingsPage({super.key});
|
||||
|
||||
@override Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = ShadTheme.of(context);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: theme.colorScheme.background,
|
||||
backgroundColor: AppColors.background,
|
||||
appBar: AppBar(
|
||||
backgroundColor: theme.colorScheme.card,
|
||||
elevation: 0,
|
||||
leading: IconButton(icon: Icon(LucideIcons.chevronLeft, color: theme.colorScheme.foreground), onPressed: () => popRoute(ref)),
|
||||
title: Text('设置', style: TextStyle(fontSize: 21, fontWeight: FontWeight.w600, color: theme.colorScheme.foreground)),
|
||||
backgroundColor: AppColors.cardBackground,
|
||||
leading: IconButton(icon: const Icon(LucideIcons.chevronLeft, color: AppColors.textPrimary), onPressed: () => popRoute(ref)),
|
||||
title: const Text('设置', style: TextStyle(fontSize: 21, fontWeight: FontWeight.w600, color: AppColors.textPrimary)),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: SafeArea(child: SingleChildScrollView(
|
||||
@@ -26,10 +24,8 @@ class SettingsPage extends ConsumerWidget {
|
||||
child: Column(children: [
|
||||
const SizedBox(height: AppTheme.sMd),
|
||||
AppMenuItem(icon: LucideIcons.bell, title: '消息通知', onTap: () => pushRoute(ref, 'notificationPrefs')),
|
||||
AppMenuItem(icon: LucideIcons.pill, title: '用药提醒', subtitle: 'mmHg / mmol/L', onTap: () => pushRoute(ref, 'medications')),
|
||||
AppMenuItem(icon: LucideIcons.database, title: '数据导出', onTap: () {}),
|
||||
AppMenuItem(icon: LucideIcons.type, title: '字体大小', trailing: 'v1.0.0', onTap: () {}),
|
||||
AppMenuItem(icon: LucideIcons.sprayCan, title: '清除缓存', subtitle: '73.2 MB', onTap: () {}),
|
||||
AppMenuItem(icon: LucideIcons.type, title: '字体大小', subtitle: '老年版', onTap: () {}),
|
||||
AppMenuItem(icon: LucideIcons.sprayCan, title: '清除缓存', 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),
|
||||
@@ -40,10 +36,10 @@ class SettingsPage extends ConsumerWidget {
|
||||
height: 50,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: theme.colorScheme.destructive.withAlpha(80)),
|
||||
border: Border.all(color: AppColors.error.withAlpha(80)),
|
||||
borderRadius: BorderRadius.circular(AppTheme.rPill),
|
||||
),
|
||||
child: Text('退出登录', style: TextStyle(fontSize: 19, color: theme.colorScheme.destructive, fontWeight: FontWeight.w500)),
|
||||
child: const Text('退出登录', style: TextStyle(fontSize: 19, color: AppColors.error, fontWeight: FontWeight.w500)),
|
||||
),
|
||||
),
|
||||
]),
|
||||
@@ -52,16 +48,15 @@ class SettingsPage extends ConsumerWidget {
|
||||
}
|
||||
|
||||
Future<void> _logout(BuildContext context, WidgetRef ref) async {
|
||||
final theme = ShadTheme.of(context);
|
||||
final ok = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppTheme.rXl)),
|
||||
title: Text('退出登录', style: TextStyle(color: theme.colorScheme.foreground)),
|
||||
content: Text('确定退出?', style: TextStyle(color: theme.colorScheme.mutedForeground)),
|
||||
title: const Text('退出登录'),
|
||||
content: const Text('确定退出?'),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(ctx, false), child: Text('取消', style: TextStyle(color: theme.colorScheme.mutedForeground))),
|
||||
TextButton(onPressed: () => Navigator.pop(ctx, true), child: Text('确定', style: TextStyle(color: theme.colorScheme.destructive))),
|
||||
TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('取消')),
|
||||
TextButton(onPressed: () => Navigator.pop(ctx, true), child: const Text('确定', style: TextStyle(color: AppColors.error))),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user