style: 统一患者端页面视觉并补齐入口跳转
- 调整全局颜色基调,提高灰色文字和边框对比度 - 新增企业级页面头部、统计块和卡片通用组件 - 优化用药管理、报告管理、个人信息、通知中心页面布局 - 新增多张页面插画和通用 UI 装饰素材 - 运动计划列表更紧凑,并新增运动计划详情路由 - 补齐医生端协议入口和原始报告查看入口 - 移除设置页未实现的字体大小和清除缓存入口
BIN
health_app/assets/branding/page_calendar_scene.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
health_app/assets/branding/page_exercise_scene.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
health_app/assets/branding/page_health_archive_scene.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
health_app/assets/branding/page_medication_scene.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
health_app/assets/branding/page_notifications_scene.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
health_app/assets/branding/page_profile_scene.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
health_app/assets/branding/page_report_scene.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
health_app/assets/branding/ui_abstract_header_bg_v1.png
Normal file
|
After Width: | Height: | Size: 920 KiB |
BIN
health_app/assets/branding/ui_corner_accent_v1.png
Normal file
|
After Width: | Height: | Size: 823 KiB |
BIN
health_app/assets/branding/ui_empty_cluster_v1.png
Normal file
|
After Width: | Height: | Size: 844 KiB |
BIN
health_app/assets/branding/ui_neutral_cluster_v1.png
Normal file
|
After Width: | Height: | Size: 912 KiB |
@@ -6,7 +6,7 @@ import 'local_database.dart';
|
||||
/// API 基础地址。可通过 --dart-define=API_BASE_URL=http://host:5000 覆盖。
|
||||
const String baseUrl = String.fromEnvironment(
|
||||
'API_BASE_URL',
|
||||
defaultValue: 'http://10.4.191.129:5000',
|
||||
defaultValue: 'http://10.4.202.36:5000',
|
||||
);
|
||||
|
||||
class ApiException implements Exception {
|
||||
|
||||
@@ -5,37 +5,37 @@ class AppColors {
|
||||
AppColors._();
|
||||
|
||||
static const Color primary = Color(0xFF7C5CFF);
|
||||
static const Color primaryLight = Color(0xFFEDE8FF);
|
||||
static const Color primarySoft = Color(0xFFF6F3FF);
|
||||
static const Color primaryDark = Color(0xFF5B3FE8);
|
||||
static const Color blueMeasure = Color(0xFF4F7CFF);
|
||||
static const Color primaryLight = Color(0xFFEDE9FF);
|
||||
static const Color primarySoft = Color(0xFFF7F4FF);
|
||||
static const Color primaryDark = Color(0xFF5B4BDB);
|
||||
static const Color blueMeasure = Color(0xFF60A5FA);
|
||||
static const Color doctorBlue = Color(0xFF3B82F6);
|
||||
static const Color auraLavender = Color(0xFFA78BFA);
|
||||
static const Color auraIndigo = Color(0xFF818CF8);
|
||||
static const Color auraOrchid = Color(0xFFC084FC);
|
||||
static const Color auraDeepIndigo = Color(0xFF6366F1);
|
||||
static const Color mintAccent = Color(0xFF2DD4BF);
|
||||
static const Color peachAccent = Color(0xFFFFA6A6);
|
||||
static const Color roseAccent = Color(0xFFF43F5E);
|
||||
static const Color meadowAccent = Color(0xFF72F874);
|
||||
static const Color sageAccent = Color(0xFF9CD9C9);
|
||||
static const Color auraLavender = Color(0xFFC4B5FD);
|
||||
static const Color auraIndigo = Color(0xFF8B5CF6);
|
||||
static const Color auraOrchid = Color(0xFFE879F9);
|
||||
static const Color auraDeepIndigo = Color(0xFF7C3AED);
|
||||
static const Color mintAccent = Color(0xFF67E8F9);
|
||||
static const Color peachAccent = Color(0xFFFBCFE8);
|
||||
static const Color roseAccent = Color(0xFFFB7185);
|
||||
static const Color meadowAccent = Color(0xFFBAE6FD);
|
||||
static const Color sageAccent = Color(0xFFE9D5FF);
|
||||
|
||||
static const Color background = Color(0xFFFAF8FF);
|
||||
static const Color backgroundSoft = Color(0xFFFFFCFF);
|
||||
static const Color background = Color(0xFFF8FAFF);
|
||||
static const Color backgroundSoft = Color(0xFFFFFFFF);
|
||||
static const Color cardBackground = Color(0xFFFFFFFF);
|
||||
static const Color cardInner = Color(0xFFF7F5FF);
|
||||
static const Color cardInner = Color(0xFFF1F5F9);
|
||||
static const Color pageGrey = background;
|
||||
static const Color iconBg = Color(0xFFF2EEFF);
|
||||
static const Color avatarBg = Color(0xFFF4EFFF);
|
||||
static const Color iconBg = Color(0xFFEFF6FF);
|
||||
static const Color avatarBg = Color(0xFFF5F3FF);
|
||||
|
||||
static const Color textPrimary = Color(0xFF1F2937);
|
||||
static const Color textSecondary = Color(0xFF667085);
|
||||
static const Color textHint = Color(0xFF98A2B3);
|
||||
static const Color textSecondary = Color(0xFF475467);
|
||||
static const Color textHint = Color(0xFF667085);
|
||||
static const Color textOnGradient = Color(0xFFFFFFFF);
|
||||
|
||||
static const Color border = Color(0xFFE5E7EB);
|
||||
static const Color borderLight = Color(0xFFF1F3F8);
|
||||
static const Color divider = Color(0xFFEFF2F7);
|
||||
static const Color border = Color(0xFFD5DCE5);
|
||||
static const Color borderLight = Color(0xFFE2E8F0);
|
||||
static const Color divider = Color(0xFFE5EAF1);
|
||||
|
||||
static const Color success = Color(0xFF16A34A);
|
||||
static const Color successLight = Color(0xFFEAF8EF);
|
||||
@@ -56,10 +56,10 @@ class AppColors {
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
Color(0xFFFFFCFF),
|
||||
Color(0xFFF9F5FF),
|
||||
Color(0xFFF5F1FF),
|
||||
Color(0xFFF2F6FF),
|
||||
Color(0xFFFFFFFF),
|
||||
Color(0xFFEFF6FF),
|
||||
Color(0xFFF5F3FF),
|
||||
Color(0xFFFDF2F8),
|
||||
],
|
||||
stops: [0.0, 0.42, 0.74, 1.0],
|
||||
);
|
||||
@@ -67,63 +67,68 @@ class AppColors {
|
||||
static const LinearGradient drawerGradient = LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [Color(0xFFFFFFFF), Color(0xFFFBF7FF), Color(0xFFF0F4FF)],
|
||||
colors: [Color(0xFFFFFFFF), Color(0xFFEFF6FF), Color(0xFFFDF2F8)],
|
||||
stops: [0.0, 0.52, 1.0],
|
||||
);
|
||||
|
||||
static const LinearGradient surfaceGradient = LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [Color(0xFFFFFFFF), Color(0xFFFCFAFF)],
|
||||
colors: [Color(0xFFFFFFFF), Color(0xFFF8FAFC)],
|
||||
);
|
||||
|
||||
static const LinearGradient softGlassGradient = LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [Color(0xF7FFFFFF), Color(0xECFFFFFF)],
|
||||
colors: [Color(0xFAFFFFFF), Color(0xF0FFFFFF)],
|
||||
);
|
||||
|
||||
static const LinearGradient roseVioletGradient = LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [Color(0xFF8B5CF6), Color(0xFFF43F5E)],
|
||||
colors: [Color(0xFF8B5CF6), Color(0xFFFB7185)],
|
||||
);
|
||||
|
||||
static const LinearGradient sageVioletGradient = LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [Color(0xFF9CD9C9), Color(0xFFAE2EF9)],
|
||||
colors: [Color(0xFF60A5FA), Color(0xFFE879F9)],
|
||||
);
|
||||
|
||||
static const LinearGradient meadowVioletGradient = LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [Color(0xFF72F874), Color(0xFF8C6BD9)],
|
||||
colors: [Color(0xFFBAE6FD), Color(0xFFC4B5FD)],
|
||||
);
|
||||
|
||||
static const LinearGradient calmHealthGradient = LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [Color(0xFF9CD9C9), Color(0xFF818CF8)],
|
||||
colors: [Color(0xFF60A5FA), Color(0xFFC4B5FD)],
|
||||
);
|
||||
|
||||
static const LinearGradient warmCareGradient = LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [Color(0xFFFFA6A6), Color(0xFFC084FC)],
|
||||
colors: [Color(0xFFFBCFE8), Color(0xFFC4B5FD)],
|
||||
);
|
||||
|
||||
static const LinearGradient actionOutlineGradient = LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [Color(0xFFC084FC), Color(0xFF818CF8), Color(0xFFC084FC), Color(0xFF818CF8)],
|
||||
colors: [
|
||||
Color(0xFFC084FC),
|
||||
Color(0xFF818CF8),
|
||||
Color(0xFFC084FC),
|
||||
Color(0xFF818CF8),
|
||||
],
|
||||
stops: [0.0, 0.3, 0.6, 1.0],
|
||||
);
|
||||
|
||||
static const LinearGradient doctorGradient = LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [Color(0xFF4F7CFF), Color(0xFF7C5CFF)],
|
||||
colors: [Color(0xFF60A5FA), Color(0xFF8B5CF6)],
|
||||
);
|
||||
|
||||
static List<BoxShadow> get cardShadow => [
|
||||
|
||||
@@ -66,6 +66,8 @@ Widget buildPage(RouteInfo route, WidgetRef ref) {
|
||||
);
|
||||
case 'exercisePlan':
|
||||
return const ExercisePlanPage();
|
||||
case 'exercisePlanDetail':
|
||||
return ExercisePlanDetailPage(id: params['id'] ?? '');
|
||||
case 'exerciseCreate':
|
||||
return const ExercisePlanCreatePage();
|
||||
case 'dietRecords':
|
||||
|
||||
@@ -305,7 +305,9 @@ class _DoctorReportDetailPageState
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () {}, // TODO: 打开文件查看器
|
||||
onPressed: () {
|
||||
pushRoute(ref, 'reportOriginal', params: {'url': fileUrl!, 'title': '原始报告'});
|
||||
},
|
||||
icon: const Icon(Icons.visibility, size: 18),
|
||||
label: const Text('查看原始报告'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
|
||||
@@ -28,8 +28,8 @@ class DoctorSettingsPage extends ConsumerWidget {
|
||||
'推送通知',
|
||||
trailing: Switch(value: true, onChanged: (_) {}),
|
||||
),
|
||||
_SettingTile(Icons.description_outlined, '隐私政策', onTap: () {}),
|
||||
_SettingTile(Icons.article_outlined, '用户协议', onTap: () {}),
|
||||
_SettingTile(Icons.description_outlined, '隐私政策', onTap: () { pushRoute(ref, 'staticText', params: {'type': 'privacy'}); }),
|
||||
_SettingTile(Icons.article_outlined, '用户协议', onTap: () { pushRoute(ref, 'staticText', params: {'type': 'terms'}); }),
|
||||
const SizedBox(height: 24),
|
||||
_SettingTile(
|
||||
Icons.delete_forever_outlined,
|
||||
|
||||
@@ -7,6 +7,7 @@ import '../../providers/data_providers.dart';
|
||||
import '../../widgets/app_empty_state.dart';
|
||||
import '../../widgets/app_future_view.dart';
|
||||
import '../../widgets/common_widgets.dart';
|
||||
import '../../widgets/enterprise_widgets.dart';
|
||||
|
||||
class MedicationListPage extends ConsumerStatefulWidget {
|
||||
const MedicationListPage({super.key});
|
||||
@@ -14,35 +15,20 @@ class MedicationListPage extends ConsumerStatefulWidget {
|
||||
ConsumerState<MedicationListPage> createState() => _MedicationListPageState();
|
||||
}
|
||||
|
||||
class _MedicationListPageState extends ConsumerState<MedicationListPage>
|
||||
with SingleTickerProviderStateMixin {
|
||||
String _filter = '';
|
||||
class _MedicationListPageState extends ConsumerState<MedicationListPage> {
|
||||
static const _medBlue = Color(0xFF60A5FA);
|
||||
static const _medCyan = Color(0xFF8B5CF6);
|
||||
Future<List<Map<String, dynamic>>>? _future;
|
||||
late final _tabCtrl = TabController(length: 3, vsync: this);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tabCtrl.addListener(() {
|
||||
if (!_tabCtrl.indexIsChanging) {
|
||||
setState(() {
|
||||
_filter = ['', 'active', 'inactive'][_tabCtrl.index];
|
||||
});
|
||||
_load();
|
||||
}
|
||||
});
|
||||
_load();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabCtrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _load() {
|
||||
setState(() {
|
||||
_future = ref.read(medicationServiceProvider).getMedications(_filter);
|
||||
_future = ref.read(medicationServiceProvider).getMedications('');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -52,8 +38,6 @@ class _MedicationListPageState extends ConsumerState<MedicationListPage>
|
||||
_load();
|
||||
}
|
||||
|
||||
static const _tabs = ['全部', '服用中', '已停药'];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GradientScaffold(
|
||||
@@ -63,24 +47,10 @@ class _MedicationListPageState extends ConsumerState<MedicationListPage>
|
||||
onPressed: () => popRoute(ref),
|
||||
),
|
||||
title: const Text('用药管理'),
|
||||
bottom: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(48),
|
||||
child: TabBar(
|
||||
controller: _tabCtrl,
|
||||
indicatorColor: AppColors.textPrimary,
|
||||
labelColor: AppColors.textPrimary,
|
||||
unselectedLabelColor: AppColors.textHint,
|
||||
labelStyle: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
tabs: _tabs.map((t) => Tab(child: Text(t))).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () => pushRoute(ref, 'medicationEdit'),
|
||||
backgroundColor: AppColors.primaryDark,
|
||||
backgroundColor: _medBlue,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
child: const Icon(Icons.add, size: 28, color: Colors.white),
|
||||
),
|
||||
@@ -96,10 +66,40 @@ class _MedicationListPageState extends ConsumerState<MedicationListPage>
|
||||
subtitle: '点击右下角➕添加药品',
|
||||
);
|
||||
}
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.fromLTRB(0, 8, 0, 80),
|
||||
itemCount: list.length,
|
||||
itemBuilder: (ctx, i) {
|
||||
final activeCount = list.where((m) => m['isActive'] == true).length;
|
||||
final reminderCount = list.where((m) {
|
||||
final times = m['timeOfDay'] as List?;
|
||||
return times != null && times.isNotEmpty;
|
||||
}).length;
|
||||
return ListView(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 88),
|
||||
children: [
|
||||
EnterpriseHeader(
|
||||
title: '用药管理',
|
||||
subtitle: '集中管理服药计划、剂量和每日打卡状态',
|
||||
icon: Icons.medication_outlined,
|
||||
color: _medBlue,
|
||||
accent: _medCyan,
|
||||
stats: [
|
||||
EnterpriseStat(
|
||||
label: '药品总数',
|
||||
value: '${list.length} 个',
|
||||
icon: Icons.inventory_2_outlined,
|
||||
),
|
||||
EnterpriseStat(
|
||||
label: '服用中',
|
||||
value: '$activeCount 个',
|
||||
icon: Icons.check_circle_outline,
|
||||
),
|
||||
EnterpriseStat(
|
||||
label: '有提醒',
|
||||
value: '$reminderCount 个',
|
||||
icon: Icons.alarm_outlined,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
...List.generate(list.length, (i) {
|
||||
final m = list[i];
|
||||
final times =
|
||||
(m['timeOfDay'] as List?)
|
||||
@@ -112,22 +112,14 @@ class _MedicationListPageState extends ConsumerState<MedicationListPage>
|
||||
key: Key(id),
|
||||
onDelete: () => _delete(id),
|
||||
onTap: () => pushRoute(ref, 'medCheckIn', params: {'id': id}),
|
||||
margin: const EdgeInsets.symmetric(
|
||||
horizontal: AppTheme.sLg,
|
||||
vertical: 4,
|
||||
),
|
||||
margin: const EdgeInsets.symmetric(vertical: 5),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(AppTheme.sLg),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(AppTheme.rLg),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: const Color(0xFF101828).withValues(alpha: 0.08),
|
||||
blurRadius: 18,
|
||||
offset: const Offset(0, 6),
|
||||
),
|
||||
],
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: AppColors.border, width: 1.1),
|
||||
boxShadow: [AppTheme.shadowLight],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
@@ -136,9 +128,13 @@ class _MedicationListPageState extends ConsumerState<MedicationListPage>
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
gradient: isActive
|
||||
? AppColors.primaryGradient
|
||||
? const LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [_medBlue, _medCyan],
|
||||
)
|
||||
: AppColors.surfaceGradient,
|
||||
borderRadius: BorderRadius.circular(AppTheme.rMd),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: AppColors.borderLight),
|
||||
),
|
||||
child: Icon(
|
||||
@@ -172,14 +168,16 @@ class _MedicationListPageState extends ConsumerState<MedicationListPage>
|
||||
vertical: 1,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.bgSoft,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
color: _medBlue.withValues(alpha: 0.08),
|
||||
borderRadius: BorderRadius.circular(
|
||||
999,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'已停',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: AppTheme.textSub,
|
||||
color: _medBlue,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -200,13 +198,14 @@ class _MedicationListPageState extends ConsumerState<MedicationListPage>
|
||||
Icon(
|
||||
Icons.chevron_right,
|
||||
size: 21,
|
||||
color: AppTheme.border,
|
||||
color: AppColors.textHint,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
}),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -147,7 +147,7 @@ class _NotificationCenterPageState
|
||||
final unread = _history?.unreadCount ?? 0;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF6F8FC),
|
||||
backgroundColor: const Color(0xFFF8FAFF),
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
@@ -196,7 +196,10 @@ class _NotificationCenterPageState
|
||||
),
|
||||
IconButton(
|
||||
tooltip: '通知设置',
|
||||
icon: const Icon(Icons.settings_outlined, color: AppColors.textPrimary),
|
||||
icon: const Icon(
|
||||
Icons.settings_outlined,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
onPressed: () => pushRoute(ref, 'notificationPrefs'),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
@@ -245,19 +248,19 @@ class _NotificationCenterPageState
|
||||
|
||||
return ListView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 32),
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 32),
|
||||
children: [
|
||||
if (unread > 0) _UnreadHint(unreadCount: unread),
|
||||
const SizedBox(height: 4),
|
||||
const SizedBox(height: 8),
|
||||
if (today.isNotEmpty) ...[
|
||||
const _SectionTitle('今天'),
|
||||
const SizedBox(height: 4),
|
||||
const SizedBox(height: 8),
|
||||
...today.map(_buildDismissible),
|
||||
],
|
||||
if (earlier.isNotEmpty) ...[
|
||||
if (today.isNotEmpty) const SizedBox(height: 12),
|
||||
if (today.isNotEmpty) const SizedBox(height: 14),
|
||||
const _SectionTitle('更早'),
|
||||
const SizedBox(height: 4),
|
||||
const SizedBox(height: 8),
|
||||
...earlier.map(_buildDismissible),
|
||||
],
|
||||
],
|
||||
@@ -265,7 +268,7 @@ class _NotificationCenterPageState
|
||||
}
|
||||
|
||||
Widget _buildDismissible(InAppNotification item) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
padding: const EdgeInsets.only(bottom: 10),
|
||||
child: Dismissible(
|
||||
key: ValueKey(item.id),
|
||||
direction: DismissDirection.endToStart,
|
||||
@@ -276,12 +279,9 @@ class _NotificationCenterPageState
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.centerLeft,
|
||||
end: Alignment.centerRight,
|
||||
colors: [
|
||||
AppColors.error.withValues(alpha: 0.6),
|
||||
AppColors.error,
|
||||
],
|
||||
colors: [AppColors.error.withValues(alpha: 0.6), AppColors.error],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: const Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -345,16 +345,32 @@ class _SectionTitle extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Padding(
|
||||
padding: const EdgeInsets.fromLTRB(4, 12, 4, 6),
|
||||
child: Text(
|
||||
padding: const EdgeInsets.fromLTRB(2, 12, 2, 6),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 4,
|
||||
height: 16,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primary,
|
||||
borderRadius: BorderRadius.circular(99),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
text,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.textHint,
|
||||
letterSpacing: 0.3,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
const Expanded(
|
||||
child: Divider(height: 1, thickness: 1, color: AppColors.divider),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -369,19 +385,20 @@ class _NotificationCard extends StatelessWidget {
|
||||
final visual = _NotificationVisual.of(item);
|
||||
return Material(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 13),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: item.isRead
|
||||
? const Color(0xFFEEF1F6)
|
||||
: visual.color.withValues(alpha: 0.20),
|
||||
? AppColors.border
|
||||
: visual.color.withValues(alpha: 0.34),
|
||||
width: item.isRead ? 1 : 1.2,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
@@ -399,13 +416,13 @@ class _NotificationCard extends StatelessWidget {
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
width: 44,
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: visual.lightColor,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(visual.icon, size: 18, color: visual.color),
|
||||
child: Icon(visual.icon, size: 22, color: visual.color),
|
||||
),
|
||||
if (!item.isRead)
|
||||
Positioned(
|
||||
@@ -423,7 +440,7 @@ class _NotificationCard extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 11),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -437,10 +454,10 @@ class _NotificationCard extends StatelessWidget {
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontSize: 16,
|
||||
fontWeight: item.isRead
|
||||
? FontWeight.w600
|
||||
: FontWeight.w700,
|
||||
? FontWeight.w700
|
||||
: FontWeight.w800,
|
||||
color: AppColors.textPrimary,
|
||||
height: 1.3,
|
||||
),
|
||||
@@ -450,19 +467,20 @@ class _NotificationCard extends StatelessWidget {
|
||||
Text(
|
||||
_formatTime(item.createdAt),
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
color: AppColors.textHint,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
const SizedBox(height: 5),
|
||||
Text(
|
||||
item.message,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontSize: 14,
|
||||
height: 1.45,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
@@ -474,8 +492,8 @@ class _NotificationCard extends StatelessWidget {
|
||||
const SizedBox(width: 4),
|
||||
const Icon(
|
||||
Icons.chevron_right_rounded,
|
||||
size: 18,
|
||||
color: AppColors.textHint,
|
||||
size: 22,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
],
|
||||
],
|
||||
@@ -514,8 +532,8 @@ class _NotificationVisual {
|
||||
case 'exercise':
|
||||
return const _NotificationVisual(
|
||||
LucideIcons.activity,
|
||||
Color(0xFF10B981),
|
||||
Color(0xFFD1FAE5),
|
||||
Color(0xFF60A5FA),
|
||||
Color(0xFFEFF6FF),
|
||||
'运动',
|
||||
);
|
||||
case 'health':
|
||||
|
||||
@@ -4,6 +4,7 @@ import '../../core/app_colors.dart';
|
||||
import '../../core/app_theme.dart';
|
||||
import '../../core/navigation_provider.dart';
|
||||
import '../../providers/auth_provider.dart';
|
||||
import '../../widgets/enterprise_widgets.dart';
|
||||
|
||||
class ProfilePage extends ConsumerWidget {
|
||||
const ProfilePage({super.key});
|
||||
@@ -34,10 +35,25 @@ class ProfilePage extends ConsumerWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_ProfileHero(
|
||||
name: name,
|
||||
phone: phone,
|
||||
avatarUrl: user?.avatarUrl,
|
||||
EnterpriseHeader(
|
||||
title: name,
|
||||
subtitle: phone.isNotEmpty ? phone : '未绑定手机',
|
||||
icon: Icons.person_rounded,
|
||||
color: const Color(0xFF38BDF8),
|
||||
accent: const Color(0xFF8B5CF6),
|
||||
stats: const [
|
||||
EnterpriseStat(
|
||||
label: '账号状态',
|
||||
value: '已登录',
|
||||
icon: Icons.verified_user_rounded,
|
||||
),
|
||||
EnterpriseStat(
|
||||
label: '健康资料',
|
||||
value: '可维护',
|
||||
icon: Icons.folder_shared_rounded,
|
||||
),
|
||||
],
|
||||
trailing: _AvatarBadge(avatarUrl: user?.avatarUrl),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
_InfoPanel(
|
||||
@@ -46,21 +62,21 @@ class ProfilePage extends ConsumerWidget {
|
||||
icon: Icons.badge_rounded,
|
||||
label: '姓名',
|
||||
value: name,
|
||||
colors: const [Color(0xFF7DD3FC), Color(0xFF2563EB)],
|
||||
colors: const [Color(0xFF7DD3FC), Color(0xFF38BDF8)],
|
||||
),
|
||||
const _SoftDivider(),
|
||||
_InfoRow(
|
||||
icon: Icons.phone_iphone_rounded,
|
||||
label: '手机号',
|
||||
value: phone.isNotEmpty ? phone : '未绑定手机',
|
||||
colors: const [Color(0xFFA78BFA), Color(0xFF7C3AED)],
|
||||
colors: const [Color(0xFFFBCFE8), Color(0xFFF472B6)],
|
||||
),
|
||||
const _SoftDivider(),
|
||||
_InfoRow(
|
||||
icon: Icons.folder_shared_rounded,
|
||||
label: '健康档案',
|
||||
value: '查看和维护基础健康资料',
|
||||
colors: const [Color(0xFF2DD4BF), Color(0xFF0F766E)],
|
||||
colors: const [Color(0xFFC4B5FD), Color(0xFF8B5CF6)],
|
||||
onTap: () => pushRoute(ref, 'healthArchive'),
|
||||
),
|
||||
],
|
||||
@@ -72,7 +88,7 @@ class ProfilePage extends ConsumerWidget {
|
||||
icon: Icons.verified_user_rounded,
|
||||
label: '隐私保护',
|
||||
value: '健康数据仅用于个人健康管理',
|
||||
colors: const [Color(0xFFFBBF24), Color(0xFFEA580C)],
|
||||
colors: const [Color(0xFFFFB4A2), Color(0xFFFB7185)],
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -131,95 +147,34 @@ class ProfilePage extends ConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _ProfileHero extends StatelessWidget {
|
||||
final String name;
|
||||
final String phone;
|
||||
class _AvatarBadge extends StatelessWidget {
|
||||
final String? avatarUrl;
|
||||
const _ProfileHero({
|
||||
required this.name,
|
||||
required this.phone,
|
||||
required this.avatarUrl,
|
||||
});
|
||||
const _AvatarBadge({required this.avatarUrl});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
width: 50,
|
||||
height: 50,
|
||||
padding: const EdgeInsets.all(2),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [Color(0xFFEEF2FF), Color(0xFFF7ECFF), Color(0xFFEFFBF9)],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
border: Border.all(color: Colors.white, width: 1.5),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: const Color(0xFF6D5DF6).withValues(alpha: 0.10),
|
||||
blurRadius: 24,
|
||||
offset: const Offset(0, 14),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 76,
|
||||
height: 76,
|
||||
padding: const EdgeInsets.all(3),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [Color(0xFF38BDF8), Color(0xFFC084FC)],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
color: const Color(0xFFF8FAFC),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: AppColors.border, width: 1.1),
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(25),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
child: avatarUrl != null
|
||||
? Image.network(avatarUrl!, fit: BoxFit.cover)
|
||||
: const ColoredBox(
|
||||
color: Colors.white,
|
||||
child: Icon(
|
||||
Icons.person_rounded,
|
||||
color: Color(0xFF7C3AED),
|
||||
size: 42,
|
||||
color: Color(0xFF38BDF8),
|
||||
size: 30,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 7),
|
||||
Text(
|
||||
phone.isNotEmpty ? phone : '未绑定手机',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -233,9 +188,9 @@ class _InfoPanel extends StatelessWidget {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.88),
|
||||
borderRadius: BorderRadius.circular(26),
|
||||
border: Border.all(color: Colors.white, width: 1.4),
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
border: Border.all(color: AppColors.borderLight),
|
||||
boxShadow: AppColors.cardShadowLight,
|
||||
),
|
||||
child: Column(children: children),
|
||||
@@ -261,7 +216,7 @@ class _InfoRow extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 2, vertical: 12),
|
||||
child: Row(
|
||||
@@ -275,7 +230,14 @@ class _InfoRow extends StatelessWidget {
|
||||
end: Alignment.bottomRight,
|
||||
colors: colors,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: colors.last.withValues(alpha: 0.18),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Icon(icon, color: Colors.white, size: 22),
|
||||
),
|
||||
@@ -324,6 +286,6 @@ class _SoftDivider extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Divider(height: 1, color: Color(0xFFF1F3F8));
|
||||
return const Divider(height: 1, color: AppColors.divider);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import '../../core/app_theme.dart';
|
||||
import '../../core/api_client.dart' show baseUrl;
|
||||
import '../../core/navigation_provider.dart';
|
||||
import '../../providers/auth_provider.dart';
|
||||
import '../../widgets/enterprise_widgets.dart';
|
||||
|
||||
final reportProvider = NotifierProvider<ReportNotifier, ReportState>(
|
||||
ReportNotifier.new,
|
||||
@@ -321,13 +322,20 @@ class ReportNotifier extends Notifier<ReportState> {
|
||||
|
||||
Future<void> reanalyzeReport(String id) async {
|
||||
try {
|
||||
final res = await ref.read(apiClientProvider).post('/api/reports/$id/reanalyze');
|
||||
final res = await ref
|
||||
.read(apiClientProvider)
|
||||
.post('/api/reports/$id/reanalyze');
|
||||
final data = res.data;
|
||||
if (data is Map && data['code'] != 0) {
|
||||
state = state.copyWith(uploadError: data['message']?.toString() ?? '重新分析失败');
|
||||
state = state.copyWith(
|
||||
uploadError: data['message']?.toString() ?? '重新分析失败',
|
||||
);
|
||||
return;
|
||||
}
|
||||
state = state.copyWith(clearCurrentAnalysis: true, clearUploadError: true);
|
||||
state = state.copyWith(
|
||||
clearCurrentAnalysis: true,
|
||||
clearUploadError: true,
|
||||
);
|
||||
await loadReports();
|
||||
fetchReportDetail(id);
|
||||
} catch (e) {
|
||||
@@ -345,6 +353,9 @@ class ReportNotifier extends Notifier<ReportState> {
|
||||
class ReportListPage extends ConsumerWidget {
|
||||
const ReportListPage({super.key});
|
||||
|
||||
static const _reportBlue = Color(0xFF8B5CF6);
|
||||
static const _reportCyan = Color(0xFF38BDF8);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final state = ref.watch(reportProvider);
|
||||
@@ -352,14 +363,17 @@ class ReportListPage extends ConsumerWidget {
|
||||
if (state.isAnalyzing) {
|
||||
return GradientScaffold(
|
||||
appBar: AppBar(
|
||||
leading: IconButton(icon: const Icon(Icons.arrow_back), onPressed: () => popRoute(ref)),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () => popRoute(ref),
|
||||
),
|
||||
title: const Text('报告管理'),
|
||||
),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const CircularProgressIndicator(color: AppColors.primary),
|
||||
const CircularProgressIndicator(color: _reportBlue),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
state.uploadingImage == null ? 'AI 正在分析报告...' : '正在上传报告...',
|
||||
@@ -389,6 +403,27 @@ class ReportListPage extends ConsumerWidget {
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
EnterpriseHeader(
|
||||
title: '报告管理',
|
||||
subtitle: '上传检查报告后自动进行 AI 结构化解读',
|
||||
icon: Icons.description_outlined,
|
||||
color: _reportBlue,
|
||||
accent: _reportCyan,
|
||||
stats: [
|
||||
EnterpriseStat(
|
||||
label: '报告总数',
|
||||
value: '${state.reports.length} 份',
|
||||
icon: Icons.folder_copy_outlined,
|
||||
),
|
||||
EnterpriseStat(
|
||||
label: '待处理',
|
||||
value:
|
||||
'${state.reports.where((r) => r.aiStatus == 'Analyzing' || r.reviewStatus != 'Reviewed').length} 份',
|
||||
icon: Icons.pending_actions_outlined,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
if (state.uploadError != null) ...[
|
||||
_buildUploadError(state.uploadError!),
|
||||
const SizedBox(height: 12),
|
||||
@@ -435,7 +470,7 @@ class ReportListPage extends ConsumerWidget {
|
||||
Widget _buildUploadButton(BuildContext context, WidgetRef ref) {
|
||||
return FloatingActionButton(
|
||||
onPressed: () => _showUploadOptions(context, ref),
|
||||
backgroundColor: AppColors.primary,
|
||||
backgroundColor: _reportBlue,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
child: const Icon(Icons.add, size: 28, color: Colors.white),
|
||||
);
|
||||
@@ -456,7 +491,7 @@ class ReportListPage extends ConsumerWidget {
|
||||
ListTile(
|
||||
leading: const Icon(
|
||||
Icons.camera_alt_outlined,
|
||||
color: AppColors.primary,
|
||||
color: _reportBlue,
|
||||
),
|
||||
title: const Text('拍照上传', style: TextStyle(fontSize: 17)),
|
||||
onTap: () async {
|
||||
@@ -474,7 +509,7 @@ class ReportListPage extends ConsumerWidget {
|
||||
ListTile(
|
||||
leading: const Icon(
|
||||
Icons.photo_library_outlined,
|
||||
color: AppColors.primary,
|
||||
color: _reportCyan,
|
||||
),
|
||||
title: const Text('从相册选择', style: TextStyle(fontSize: 17)),
|
||||
onTap: () async {
|
||||
@@ -505,13 +540,13 @@ class ReportListPage extends ConsumerWidget {
|
||||
width: 100,
|
||||
height: 100,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.cardInner,
|
||||
color: _reportBlue.withValues(alpha: 0.08),
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.description_outlined,
|
||||
size: 44,
|
||||
color: AppColors.textHint,
|
||||
color: _reportBlue,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
@@ -545,38 +580,34 @@ class ReportListPage extends ConsumerWidget {
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: const Color(0xFFD5DCE5)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: const Color(0xFF101828).withValues(alpha: 0.08),
|
||||
blurRadius: 18,
|
||||
offset: const Offset(0, 6),
|
||||
),
|
||||
],
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: AppColors.border, width: 1.1),
|
||||
boxShadow: [AppTheme.shadowLight],
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: InkWell(
|
||||
onTap: () => pushRoute(ref, 'aiAnalysis', params: {'id': report.id}),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(14),
|
||||
padding: const EdgeInsets.all(15),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.iconBg,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: AppColors.borderLight),
|
||||
color: _reportBlue.withValues(alpha: 0.10),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(
|
||||
color: _reportBlue.withValues(alpha: 0.10),
|
||||
),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.description_outlined,
|
||||
size: 26,
|
||||
color: AppColors.primary,
|
||||
color: _reportBlue,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
@@ -586,9 +617,11 @@ class ReportListPage extends ConsumerWidget {
|
||||
children: [
|
||||
Text(
|
||||
displayTitle,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
@@ -619,17 +652,15 @@ class ReportListPage extends ConsumerWidget {
|
||||
),
|
||||
_buildStatusBadge(report),
|
||||
const SizedBox(width: 4),
|
||||
GestureDetector(
|
||||
onTap: () => _confirmDelete(context, ref, report.id),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(6),
|
||||
child: Icon(
|
||||
IconButton(
|
||||
onPressed: () => _confirmDelete(context, ref, report.id),
|
||||
visualDensity: VisualDensity.compact,
|
||||
icon: const Icon(
|
||||
Icons.delete_outline,
|
||||
size: 20,
|
||||
color: AppColors.textHint,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -663,26 +694,39 @@ class ReportListPage extends ConsumerWidget {
|
||||
|
||||
Widget _buildStatusBadge(ReportItem report) {
|
||||
final (label, bg, fg) = switch (report.status) {
|
||||
_ when report.reviewStatus == 'Reviewed' => ('已审核', AppColors.successLight, AppColors.success),
|
||||
_ when report.aiStatus == 'Analyzing' => ('分析中', AppColors.cardInner, AppColors.textHint),
|
||||
_ when report.aiStatus == 'Failed' => ('分析失败', AppColors.error.withValues(alpha: 0.08), AppColors.error),
|
||||
_ when report.aiStatus == 'Succeeded' => ('待审核', AppColors.warningLight, AppColors.warning),
|
||||
_ => ('分析中', AppColors.cardInner, AppColors.textHint),
|
||||
_ when report.reviewStatus == 'Reviewed' => (
|
||||
'已审核',
|
||||
AppColors.successLight,
|
||||
AppColors.success,
|
||||
),
|
||||
_ when report.aiStatus == 'Analyzing' => (
|
||||
'分析中',
|
||||
_reportBlue.withValues(alpha: 0.08),
|
||||
_reportBlue,
|
||||
),
|
||||
_ when report.aiStatus == 'Failed' => (
|
||||
'分析失败',
|
||||
AppColors.error.withValues(alpha: 0.08),
|
||||
AppColors.error,
|
||||
),
|
||||
_ when report.aiStatus == 'Succeeded' => (
|
||||
'待审核',
|
||||
AppColors.warningLight,
|
||||
AppColors.warning,
|
||||
),
|
||||
_ => ('分析中', _reportBlue.withValues(alpha: 0.08), _reportBlue),
|
||||
};
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: bg,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
border: Border.all(color: fg.withValues(alpha: 0.12)),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: fg,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
style: TextStyle(fontSize: 13, color: fg, fontWeight: FontWeight.w700),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -719,18 +763,17 @@ class ReportOriginalPage extends StatelessWidget {
|
||||
final String url;
|
||||
final String title;
|
||||
|
||||
const ReportOriginalPage({
|
||||
super.key,
|
||||
required this.url,
|
||||
this.title = '原始报告',
|
||||
});
|
||||
const ReportOriginalPage({super.key, required this.url, this.title = '原始报告'});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final imageUrl = _absoluteUrl(url);
|
||||
return GradientScaffold(
|
||||
appBar: AppBar(
|
||||
leading: IconButton(icon: const Icon(Icons.arrow_back), onPressed: () => Navigator.pop(context)),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
title: Text(title),
|
||||
),
|
||||
body: Center(
|
||||
|
||||
@@ -55,21 +55,6 @@ class SettingsPage extends ConsumerWidget {
|
||||
onTap: () => pushRoute(ref, 'notificationPrefs'),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_SettingsTile(
|
||||
icon: LucideIcons.type,
|
||||
title: '字体大小',
|
||||
subtitle: '老年友好',
|
||||
colors: const [Color(0xFFFBBF24), Color(0xFFEA580C)],
|
||||
onTap: () {},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_SettingsTile(
|
||||
icon: LucideIcons.sprayCan,
|
||||
title: '清除缓存',
|
||||
colors: const [Color(0xFF2DD4BF), Color(0xFF0F766E)],
|
||||
onTap: () {},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_SettingsTile(
|
||||
icon: LucideIcons.info,
|
||||
title: '关于小脉健康',
|
||||
|
||||
212
health_app/lib/widgets/enterprise_widgets.dart
Normal file
@@ -0,0 +1,212 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../core/app_colors.dart';
|
||||
import '../core/app_theme.dart';
|
||||
|
||||
class EnterpriseStat {
|
||||
final String label;
|
||||
final String value;
|
||||
final IconData? icon;
|
||||
|
||||
const EnterpriseStat({required this.label, required this.value, this.icon});
|
||||
}
|
||||
|
||||
class EnterpriseHeader extends StatelessWidget {
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
final Color accent;
|
||||
final List<EnterpriseStat> stats;
|
||||
final Widget? trailing;
|
||||
|
||||
const EnterpriseHeader({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
required this.icon,
|
||||
required this.color,
|
||||
required this.accent,
|
||||
this.stats = const [],
|
||||
this.trailing,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
for (var i = 0; i < stats.length; i++) ...[
|
||||
Expanded(
|
||||
child: _HeaderStat(
|
||||
stat: stats[i],
|
||||
color: i.isEven ? color : accent,
|
||||
accent: i.isEven ? accent : color,
|
||||
),
|
||||
),
|
||||
if (i != stats.length - 1) const SizedBox(width: 8),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _HeaderStat extends StatelessWidget {
|
||||
final EnterpriseStat stat;
|
||||
final Color color;
|
||||
final Color accent;
|
||||
|
||||
const _HeaderStat({
|
||||
required this.stat,
|
||||
required this.color,
|
||||
required this.accent,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
constraints: const BoxConstraints(minHeight: 54),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 9),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
color.withValues(alpha: 0.10),
|
||||
accent.withValues(alpha: 0.06),
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: AppColors.borderLight, width: 1.1),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
if (stat.icon != null) ...[
|
||||
Icon(stat.icon, color: color, size: 16),
|
||||
const SizedBox(width: 6),
|
||||
],
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
stat.value,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: AppColors.textPrimary,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
stat.label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: AppColors.textSecondary,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class EnterpriseSectionCard extends StatelessWidget {
|
||||
final String? title;
|
||||
final IconData? icon;
|
||||
final Color color;
|
||||
final Widget child;
|
||||
final EdgeInsetsGeometry padding;
|
||||
final Widget? trailing;
|
||||
|
||||
const EnterpriseSectionCard({
|
||||
super.key,
|
||||
this.title,
|
||||
this.icon,
|
||||
required this.color,
|
||||
required this.child,
|
||||
this.trailing,
|
||||
this.padding = const EdgeInsets.all(16),
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: padding,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: AppColors.border, width: 1.1),
|
||||
boxShadow: [AppTheme.shadowLight],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (title != null) ...[
|
||||
Row(
|
||||
children: [
|
||||
if (icon != null) ...[
|
||||
Container(
|
||||
width: 34,
|
||||
height: 34,
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 0.10),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(icon, size: 19, color: color),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
],
|
||||
Expanded(
|
||||
child: Text(
|
||||
title!,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
?trailing,
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
],
|
||||
child,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class EnterpriseFab extends StatelessWidget {
|
||||
final VoidCallback onPressed;
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
|
||||
const EnterpriseFab({
|
||||
super.key,
|
||||
required this.onPressed,
|
||||
required this.icon,
|
||||
required this.color,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FloatingActionButton(
|
||||
onPressed: onPressed,
|
||||
backgroundColor: color,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
child: Icon(icon, size: 26, color: Colors.white),
|
||||
);
|
||||
}
|
||||
}
|
||||