style: 统一患者端页面视觉并补齐入口跳转

- 调整全局颜色基调,提高灰色文字和边框对比度
- 新增企业级页面头部、统计块和卡片通用组件
- 优化用药管理、报告管理、个人信息、通知中心页面布局
- 新增多张页面插画和通用 UI 装饰素材
- 运动计划列表更紧凑,并新增运动计划详情路由
- 补齐医生端协议入口和原始报告查看入口
- 移除设置页未实现的字体大小和清除缓存入口
This commit is contained in:
MingNian
2026-06-25 22:13:42 +08:00
parent 53ce19e8c3
commit 18df2f3285
23 changed files with 1266 additions and 470 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 920 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 823 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 844 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 912 KiB

View File

@@ -6,7 +6,7 @@ import 'local_database.dart';
/// API 基础地址。可通过 --dart-define=API_BASE_URL=http://host:5000 覆盖。 /// API 基础地址。可通过 --dart-define=API_BASE_URL=http://host:5000 覆盖。
const String baseUrl = String.fromEnvironment( const String baseUrl = String.fromEnvironment(
'API_BASE_URL', 'API_BASE_URL',
defaultValue: 'http://10.4.191.129:5000', defaultValue: 'http://10.4.202.36:5000',
); );
class ApiException implements Exception { class ApiException implements Exception {

View File

@@ -5,37 +5,37 @@ class AppColors {
AppColors._(); AppColors._();
static const Color primary = Color(0xFF7C5CFF); static const Color primary = Color(0xFF7C5CFF);
static const Color primaryLight = Color(0xFFEDE8FF); static const Color primaryLight = Color(0xFFEDE9FF);
static const Color primarySoft = Color(0xFFF6F3FF); static const Color primarySoft = Color(0xFFF7F4FF);
static const Color primaryDark = Color(0xFF5B3FE8); static const Color primaryDark = Color(0xFF5B4BDB);
static const Color blueMeasure = Color(0xFF4F7CFF); static const Color blueMeasure = Color(0xFF60A5FA);
static const Color doctorBlue = Color(0xFF3B82F6); static const Color doctorBlue = Color(0xFF3B82F6);
static const Color auraLavender = Color(0xFFA78BFA); static const Color auraLavender = Color(0xFFC4B5FD);
static const Color auraIndigo = Color(0xFF818CF8); static const Color auraIndigo = Color(0xFF8B5CF6);
static const Color auraOrchid = Color(0xFFC084FC); static const Color auraOrchid = Color(0xFFE879F9);
static const Color auraDeepIndigo = Color(0xFF6366F1); static const Color auraDeepIndigo = Color(0xFF7C3AED);
static const Color mintAccent = Color(0xFF2DD4BF); static const Color mintAccent = Color(0xFF67E8F9);
static const Color peachAccent = Color(0xFFFFA6A6); static const Color peachAccent = Color(0xFFFBCFE8);
static const Color roseAccent = Color(0xFFF43F5E); static const Color roseAccent = Color(0xFFFB7185);
static const Color meadowAccent = Color(0xFF72F874); static const Color meadowAccent = Color(0xFFBAE6FD);
static const Color sageAccent = Color(0xFF9CD9C9); static const Color sageAccent = Color(0xFFE9D5FF);
static const Color background = Color(0xFFFAF8FF); static const Color background = Color(0xFFF8FAFF);
static const Color backgroundSoft = Color(0xFFFFFCFF); static const Color backgroundSoft = Color(0xFFFFFFFF);
static const Color cardBackground = 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 pageGrey = background;
static const Color iconBg = Color(0xFFF2EEFF); static const Color iconBg = Color(0xFFEFF6FF);
static const Color avatarBg = Color(0xFFF4EFFF); static const Color avatarBg = Color(0xFFF5F3FF);
static const Color textPrimary = Color(0xFF1F2937); static const Color textPrimary = Color(0xFF1F2937);
static const Color textSecondary = Color(0xFF667085); static const Color textSecondary = Color(0xFF475467);
static const Color textHint = Color(0xFF98A2B3); static const Color textHint = Color(0xFF667085);
static const Color textOnGradient = Color(0xFFFFFFFF); static const Color textOnGradient = Color(0xFFFFFFFF);
static const Color border = Color(0xFFE5E7EB); static const Color border = Color(0xFFD5DCE5);
static const Color borderLight = Color(0xFFF1F3F8); static const Color borderLight = Color(0xFFE2E8F0);
static const Color divider = Color(0xFFEFF2F7); static const Color divider = Color(0xFFE5EAF1);
static const Color success = Color(0xFF16A34A); static const Color success = Color(0xFF16A34A);
static const Color successLight = Color(0xFFEAF8EF); static const Color successLight = Color(0xFFEAF8EF);
@@ -56,10 +56,10 @@ class AppColors {
begin: Alignment.topLeft, begin: Alignment.topLeft,
end: Alignment.bottomRight, end: Alignment.bottomRight,
colors: [ colors: [
Color(0xFFFFFCFF), Color(0xFFFFFFFF),
Color(0xFFF9F5FF), Color(0xFFEFF6FF),
Color(0xFFF5F1FF), Color(0xFFF5F3FF),
Color(0xFFF2F6FF), Color(0xFFFDF2F8),
], ],
stops: [0.0, 0.42, 0.74, 1.0], stops: [0.0, 0.42, 0.74, 1.0],
); );
@@ -67,63 +67,68 @@ class AppColors {
static const LinearGradient drawerGradient = LinearGradient( static const LinearGradient drawerGradient = LinearGradient(
begin: Alignment.topLeft, begin: Alignment.topLeft,
end: Alignment.bottomRight, end: Alignment.bottomRight,
colors: [Color(0xFFFFFFFF), Color(0xFFFBF7FF), Color(0xFFF0F4FF)], colors: [Color(0xFFFFFFFF), Color(0xFFEFF6FF), Color(0xFFFDF2F8)],
stops: [0.0, 0.52, 1.0], stops: [0.0, 0.52, 1.0],
); );
static const LinearGradient surfaceGradient = LinearGradient( static const LinearGradient surfaceGradient = LinearGradient(
begin: Alignment.topLeft, begin: Alignment.topLeft,
end: Alignment.bottomRight, end: Alignment.bottomRight,
colors: [Color(0xFFFFFFFF), Color(0xFFFCFAFF)], colors: [Color(0xFFFFFFFF), Color(0xFFF8FAFC)],
); );
static const LinearGradient softGlassGradient = LinearGradient( static const LinearGradient softGlassGradient = LinearGradient(
begin: Alignment.topLeft, begin: Alignment.topLeft,
end: Alignment.bottomRight, end: Alignment.bottomRight,
colors: [Color(0xF7FFFFFF), Color(0xECFFFFFF)], colors: [Color(0xFAFFFFFF), Color(0xF0FFFFFF)],
); );
static const LinearGradient roseVioletGradient = LinearGradient( static const LinearGradient roseVioletGradient = LinearGradient(
begin: Alignment.topLeft, begin: Alignment.topLeft,
end: Alignment.bottomRight, end: Alignment.bottomRight,
colors: [Color(0xFF8B5CF6), Color(0xFFF43F5E)], colors: [Color(0xFF8B5CF6), Color(0xFFFB7185)],
); );
static const LinearGradient sageVioletGradient = LinearGradient( static const LinearGradient sageVioletGradient = LinearGradient(
begin: Alignment.topLeft, begin: Alignment.topLeft,
end: Alignment.bottomRight, end: Alignment.bottomRight,
colors: [Color(0xFF9CD9C9), Color(0xFFAE2EF9)], colors: [Color(0xFF60A5FA), Color(0xFFE879F9)],
); );
static const LinearGradient meadowVioletGradient = LinearGradient( static const LinearGradient meadowVioletGradient = LinearGradient(
begin: Alignment.topLeft, begin: Alignment.topLeft,
end: Alignment.bottomRight, end: Alignment.bottomRight,
colors: [Color(0xFF72F874), Color(0xFF8C6BD9)], colors: [Color(0xFFBAE6FD), Color(0xFFC4B5FD)],
); );
static const LinearGradient calmHealthGradient = LinearGradient( static const LinearGradient calmHealthGradient = LinearGradient(
begin: Alignment.topLeft, begin: Alignment.topLeft,
end: Alignment.bottomRight, end: Alignment.bottomRight,
colors: [Color(0xFF9CD9C9), Color(0xFF818CF8)], colors: [Color(0xFF60A5FA), Color(0xFFC4B5FD)],
); );
static const LinearGradient warmCareGradient = LinearGradient( static const LinearGradient warmCareGradient = LinearGradient(
begin: Alignment.topLeft, begin: Alignment.topLeft,
end: Alignment.bottomRight, end: Alignment.bottomRight,
colors: [Color(0xFFFFA6A6), Color(0xFFC084FC)], colors: [Color(0xFFFBCFE8), Color(0xFFC4B5FD)],
); );
static const LinearGradient actionOutlineGradient = LinearGradient( static const LinearGradient actionOutlineGradient = LinearGradient(
begin: Alignment.topLeft, begin: Alignment.topLeft,
end: Alignment.bottomRight, 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], stops: [0.0, 0.3, 0.6, 1.0],
); );
static const LinearGradient doctorGradient = LinearGradient( static const LinearGradient doctorGradient = LinearGradient(
begin: Alignment.topLeft, begin: Alignment.topLeft,
end: Alignment.bottomRight, end: Alignment.bottomRight,
colors: [Color(0xFF4F7CFF), Color(0xFF7C5CFF)], colors: [Color(0xFF60A5FA), Color(0xFF8B5CF6)],
); );
static List<BoxShadow> get cardShadow => [ static List<BoxShadow> get cardShadow => [

View File

@@ -66,6 +66,8 @@ Widget buildPage(RouteInfo route, WidgetRef ref) {
); );
case 'exercisePlan': case 'exercisePlan':
return const ExercisePlanPage(); return const ExercisePlanPage();
case 'exercisePlanDetail':
return ExercisePlanDetailPage(id: params['id'] ?? '');
case 'exerciseCreate': case 'exerciseCreate':
return const ExercisePlanCreatePage(); return const ExercisePlanCreatePage();
case 'dietRecords': case 'dietRecords':

View File

@@ -305,7 +305,9 @@ class _DoctorReportDetailPageState
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,
child: OutlinedButton.icon( child: OutlinedButton.icon(
onPressed: () {}, // TODO: 打开文件查看器 onPressed: () {
pushRoute(ref, 'reportOriginal', params: {'url': fileUrl!, 'title': '原始报告'});
},
icon: const Icon(Icons.visibility, size: 18), icon: const Icon(Icons.visibility, size: 18),
label: const Text('查看原始报告'), label: const Text('查看原始报告'),
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(

View File

@@ -28,8 +28,8 @@ class DoctorSettingsPage extends ConsumerWidget {
'推送通知', '推送通知',
trailing: Switch(value: true, onChanged: (_) {}), trailing: Switch(value: true, onChanged: (_) {}),
), ),
_SettingTile(Icons.description_outlined, '隐私政策', onTap: () {}), _SettingTile(Icons.description_outlined, '隐私政策', onTap: () { pushRoute(ref, 'staticText', params: {'type': 'privacy'}); }),
_SettingTile(Icons.article_outlined, '用户协议', onTap: () {}), _SettingTile(Icons.article_outlined, '用户协议', onTap: () { pushRoute(ref, 'staticText', params: {'type': 'terms'}); }),
const SizedBox(height: 24), const SizedBox(height: 24),
_SettingTile( _SettingTile(
Icons.delete_forever_outlined, Icons.delete_forever_outlined,

View File

@@ -7,6 +7,7 @@ import '../../providers/data_providers.dart';
import '../../widgets/app_empty_state.dart'; import '../../widgets/app_empty_state.dart';
import '../../widgets/app_future_view.dart'; import '../../widgets/app_future_view.dart';
import '../../widgets/common_widgets.dart'; import '../../widgets/common_widgets.dart';
import '../../widgets/enterprise_widgets.dart';
class MedicationListPage extends ConsumerStatefulWidget { class MedicationListPage extends ConsumerStatefulWidget {
const MedicationListPage({super.key}); const MedicationListPage({super.key});
@@ -14,35 +15,20 @@ class MedicationListPage extends ConsumerStatefulWidget {
ConsumerState<MedicationListPage> createState() => _MedicationListPageState(); ConsumerState<MedicationListPage> createState() => _MedicationListPageState();
} }
class _MedicationListPageState extends ConsumerState<MedicationListPage> class _MedicationListPageState extends ConsumerState<MedicationListPage> {
with SingleTickerProviderStateMixin { static const _medBlue = Color(0xFF60A5FA);
String _filter = ''; static const _medCyan = Color(0xFF8B5CF6);
Future<List<Map<String, dynamic>>>? _future; Future<List<Map<String, dynamic>>>? _future;
late final _tabCtrl = TabController(length: 3, vsync: this);
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_tabCtrl.addListener(() {
if (!_tabCtrl.indexIsChanging) {
setState(() {
_filter = ['', 'active', 'inactive'][_tabCtrl.index];
});
_load(); _load();
} }
});
_load();
}
@override
void dispose() {
_tabCtrl.dispose();
super.dispose();
}
void _load() { void _load() {
setState(() { setState(() {
_future = ref.read(medicationServiceProvider).getMedications(_filter); _future = ref.read(medicationServiceProvider).getMedications('');
}); });
} }
@@ -52,8 +38,6 @@ class _MedicationListPageState extends ConsumerState<MedicationListPage>
_load(); _load();
} }
static const _tabs = ['全部', '服用中', '已停药'];
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GradientScaffold( return GradientScaffold(
@@ -63,24 +47,10 @@ class _MedicationListPageState extends ConsumerState<MedicationListPage>
onPressed: () => popRoute(ref), onPressed: () => popRoute(ref),
), ),
title: const Text('用药管理'), 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( floatingActionButton: FloatingActionButton(
onPressed: () => pushRoute(ref, 'medicationEdit'), onPressed: () => pushRoute(ref, 'medicationEdit'),
backgroundColor: AppColors.primaryDark, backgroundColor: _medBlue,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: const Icon(Icons.add, size: 28, color: Colors.white), child: const Icon(Icons.add, size: 28, color: Colors.white),
), ),
@@ -96,10 +66,40 @@ class _MedicationListPageState extends ConsumerState<MedicationListPage>
subtitle: '点击右下角➕添加药品', subtitle: '点击右下角➕添加药品',
); );
} }
return ListView.builder( final activeCount = list.where((m) => m['isActive'] == true).length;
padding: const EdgeInsets.fromLTRB(0, 8, 0, 80), final reminderCount = list.where((m) {
itemCount: list.length, final times = m['timeOfDay'] as List?;
itemBuilder: (ctx, i) { 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 m = list[i];
final times = final times =
(m['timeOfDay'] as List?) (m['timeOfDay'] as List?)
@@ -112,22 +112,14 @@ class _MedicationListPageState extends ConsumerState<MedicationListPage>
key: Key(id), key: Key(id),
onDelete: () => _delete(id), onDelete: () => _delete(id),
onTap: () => pushRoute(ref, 'medCheckIn', params: {'id': id}), onTap: () => pushRoute(ref, 'medCheckIn', params: {'id': id}),
margin: const EdgeInsets.symmetric( margin: const EdgeInsets.symmetric(vertical: 5),
horizontal: AppTheme.sLg,
vertical: 4,
),
child: Container( child: Container(
padding: const EdgeInsets.all(AppTheme.sLg), padding: const EdgeInsets.all(AppTheme.sLg),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(AppTheme.rLg), borderRadius: BorderRadius.circular(16),
boxShadow: [ border: Border.all(color: AppColors.border, width: 1.1),
BoxShadow( boxShadow: [AppTheme.shadowLight],
color: const Color(0xFF101828).withValues(alpha: 0.08),
blurRadius: 18,
offset: const Offset(0, 6),
),
],
), ),
child: Row( child: Row(
children: [ children: [
@@ -136,9 +128,13 @@ class _MedicationListPageState extends ConsumerState<MedicationListPage>
height: 44, height: 44,
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: isActive gradient: isActive
? AppColors.primaryGradient ? const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [_medBlue, _medCyan],
)
: AppColors.surfaceGradient, : AppColors.surfaceGradient,
borderRadius: BorderRadius.circular(AppTheme.rMd), borderRadius: BorderRadius.circular(14),
border: Border.all(color: AppColors.borderLight), border: Border.all(color: AppColors.borderLight),
), ),
child: Icon( child: Icon(
@@ -172,14 +168,16 @@ class _MedicationListPageState extends ConsumerState<MedicationListPage>
vertical: 1, vertical: 1,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppTheme.bgSoft, color: _medBlue.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(
999,
),
), ),
child: Text( child: Text(
'已停', '已停',
style: TextStyle( style: TextStyle(
fontSize: 13, fontSize: 13,
color: AppTheme.textSub, color: _medBlue,
), ),
), ),
), ),
@@ -200,13 +198,14 @@ class _MedicationListPageState extends ConsumerState<MedicationListPage>
Icon( Icon(
Icons.chevron_right, Icons.chevron_right,
size: 21, size: 21,
color: AppTheme.border, color: AppColors.textHint,
), ),
], ],
), ),
), ),
); );
}, }),
],
); );
}, },
), ),

View File

@@ -147,7 +147,7 @@ class _NotificationCenterPageState
final unread = _history?.unreadCount ?? 0; final unread = _history?.unreadCount ?? 0;
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFF6F8FC), backgroundColor: const Color(0xFFF8FAFF),
appBar: AppBar( appBar: AppBar(
backgroundColor: Colors.white, backgroundColor: Colors.white,
elevation: 0, elevation: 0,
@@ -196,7 +196,10 @@ class _NotificationCenterPageState
), ),
IconButton( IconButton(
tooltip: '通知设置', tooltip: '通知设置',
icon: const Icon(Icons.settings_outlined, color: AppColors.textPrimary), icon: const Icon(
Icons.settings_outlined,
color: AppColors.textPrimary,
),
onPressed: () => pushRoute(ref, 'notificationPrefs'), onPressed: () => pushRoute(ref, 'notificationPrefs'),
), ),
const SizedBox(width: 4), const SizedBox(width: 4),
@@ -245,19 +248,19 @@ class _NotificationCenterPageState
return ListView( return ListView(
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.fromLTRB(16, 8, 16, 32), padding: const EdgeInsets.fromLTRB(16, 12, 16, 32),
children: [ children: [
if (unread > 0) _UnreadHint(unreadCount: unread), if (unread > 0) _UnreadHint(unreadCount: unread),
const SizedBox(height: 4), const SizedBox(height: 8),
if (today.isNotEmpty) ...[ if (today.isNotEmpty) ...[
const _SectionTitle('今天'), const _SectionTitle('今天'),
const SizedBox(height: 4), const SizedBox(height: 8),
...today.map(_buildDismissible), ...today.map(_buildDismissible),
], ],
if (earlier.isNotEmpty) ...[ if (earlier.isNotEmpty) ...[
if (today.isNotEmpty) const SizedBox(height: 12), if (today.isNotEmpty) const SizedBox(height: 14),
const _SectionTitle('更早'), const _SectionTitle('更早'),
const SizedBox(height: 4), const SizedBox(height: 8),
...earlier.map(_buildDismissible), ...earlier.map(_buildDismissible),
], ],
], ],
@@ -265,7 +268,7 @@ class _NotificationCenterPageState
} }
Widget _buildDismissible(InAppNotification item) => Padding( Widget _buildDismissible(InAppNotification item) => Padding(
padding: const EdgeInsets.only(bottom: 12), padding: const EdgeInsets.only(bottom: 10),
child: Dismissible( child: Dismissible(
key: ValueKey(item.id), key: ValueKey(item.id),
direction: DismissDirection.endToStart, direction: DismissDirection.endToStart,
@@ -276,12 +279,9 @@ class _NotificationCenterPageState
gradient: LinearGradient( gradient: LinearGradient(
begin: Alignment.centerLeft, begin: Alignment.centerLeft,
end: Alignment.centerRight, end: Alignment.centerRight,
colors: [ colors: [AppColors.error.withValues(alpha: 0.6), AppColors.error],
AppColors.error.withValues(alpha: 0.6),
AppColors.error,
],
), ),
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(16),
), ),
child: const Row( child: const Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -345,16 +345,32 @@ class _SectionTitle extends StatelessWidget {
@override @override
Widget build(BuildContext context) => Padding( Widget build(BuildContext context) => Padding(
padding: const EdgeInsets.fromLTRB(4, 12, 4, 6), padding: const EdgeInsets.fromLTRB(2, 12, 2, 6),
child: Text( child: Row(
children: [
Container(
width: 4,
height: 16,
decoration: BoxDecoration(
color: AppColors.primary,
borderRadius: BorderRadius.circular(99),
),
),
const SizedBox(width: 8),
Text(
text, text,
style: const TextStyle( style: const TextStyle(
fontSize: 13, fontSize: 15,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w800,
color: AppColors.textHint, color: AppColors.textPrimary,
letterSpacing: 0.3,
), ),
), ),
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); final visual = _NotificationVisual.of(item);
return Material( return Material(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(14), borderRadius: BorderRadius.circular(16),
child: InkWell( child: InkWell(
onTap: onTap, onTap: onTap,
borderRadius: BorderRadius.circular(14), borderRadius: BorderRadius.circular(16),
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 13),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(14), borderRadius: BorderRadius.circular(16),
border: Border.all( border: Border.all(
color: item.isRead color: item.isRead
? const Color(0xFFEEF1F6) ? AppColors.border
: visual.color.withValues(alpha: 0.20), : visual.color.withValues(alpha: 0.34),
width: item.isRead ? 1 : 1.2,
), ),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
@@ -399,13 +416,13 @@ class _NotificationCard extends StatelessWidget {
clipBehavior: Clip.none, clipBehavior: Clip.none,
children: [ children: [
Container( Container(
width: 36, width: 44,
height: 36, height: 44,
decoration: BoxDecoration( decoration: BoxDecoration(
color: visual.lightColor, 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) if (!item.isRead)
Positioned( Positioned(
@@ -423,7 +440,7 @@ class _NotificationCard extends StatelessWidget {
), ),
], ],
), ),
const SizedBox(width: 11), const SizedBox(width: 12),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -437,10 +454,10 @@ class _NotificationCard extends StatelessWidget {
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 16,
fontWeight: item.isRead fontWeight: item.isRead
? FontWeight.w600 ? FontWeight.w700
: FontWeight.w700, : FontWeight.w800,
color: AppColors.textPrimary, color: AppColors.textPrimary,
height: 1.3, height: 1.3,
), ),
@@ -450,19 +467,20 @@ class _NotificationCard extends StatelessWidget {
Text( Text(
_formatTime(item.createdAt), _formatTime(item.createdAt),
style: const TextStyle( style: const TextStyle(
fontSize: 11, fontSize: 12,
color: AppColors.textHint, fontWeight: FontWeight.w600,
color: AppColors.textSecondary,
), ),
), ),
], ],
), ),
const SizedBox(height: 2), const SizedBox(height: 5),
Text( Text(
item.message, item.message,
maxLines: 2, maxLines: 2,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: const TextStyle( style: const TextStyle(
fontSize: 12, fontSize: 14,
height: 1.45, height: 1.45,
color: AppColors.textSecondary, color: AppColors.textSecondary,
), ),
@@ -474,8 +492,8 @@ class _NotificationCard extends StatelessWidget {
const SizedBox(width: 4), const SizedBox(width: 4),
const Icon( const Icon(
Icons.chevron_right_rounded, Icons.chevron_right_rounded,
size: 18, size: 22,
color: AppColors.textHint, color: AppColors.textSecondary,
), ),
], ],
], ],
@@ -514,8 +532,8 @@ class _NotificationVisual {
case 'exercise': case 'exercise':
return const _NotificationVisual( return const _NotificationVisual(
LucideIcons.activity, LucideIcons.activity,
Color(0xFF10B981), Color(0xFF60A5FA),
Color(0xFFD1FAE5), Color(0xFFEFF6FF),
'运动', '运动',
); );
case 'health': case 'health':

View File

@@ -4,6 +4,7 @@ import '../../core/app_colors.dart';
import '../../core/app_theme.dart'; import '../../core/app_theme.dart';
import '../../core/navigation_provider.dart'; import '../../core/navigation_provider.dart';
import '../../providers/auth_provider.dart'; import '../../providers/auth_provider.dart';
import '../../widgets/enterprise_widgets.dart';
class ProfilePage extends ConsumerWidget { class ProfilePage extends ConsumerWidget {
const ProfilePage({super.key}); const ProfilePage({super.key});
@@ -34,10 +35,25 @@ class ProfilePage extends ConsumerWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
_ProfileHero( EnterpriseHeader(
name: name, title: name,
phone: phone, subtitle: phone.isNotEmpty ? phone : '未绑定手机',
avatarUrl: user?.avatarUrl, 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), const SizedBox(height: 18),
_InfoPanel( _InfoPanel(
@@ -46,21 +62,21 @@ class ProfilePage extends ConsumerWidget {
icon: Icons.badge_rounded, icon: Icons.badge_rounded,
label: '姓名', label: '姓名',
value: name, value: name,
colors: const [Color(0xFF7DD3FC), Color(0xFF2563EB)], colors: const [Color(0xFF7DD3FC), Color(0xFF38BDF8)],
), ),
const _SoftDivider(), const _SoftDivider(),
_InfoRow( _InfoRow(
icon: Icons.phone_iphone_rounded, icon: Icons.phone_iphone_rounded,
label: '手机号', label: '手机号',
value: phone.isNotEmpty ? phone : '未绑定手机', value: phone.isNotEmpty ? phone : '未绑定手机',
colors: const [Color(0xFFA78BFA), Color(0xFF7C3AED)], colors: const [Color(0xFFFBCFE8), Color(0xFFF472B6)],
), ),
const _SoftDivider(), const _SoftDivider(),
_InfoRow( _InfoRow(
icon: Icons.folder_shared_rounded, icon: Icons.folder_shared_rounded,
label: '健康档案', label: '健康档案',
value: '查看和维护基础健康资料', value: '查看和维护基础健康资料',
colors: const [Color(0xFF2DD4BF), Color(0xFF0F766E)], colors: const [Color(0xFFC4B5FD), Color(0xFF8B5CF6)],
onTap: () => pushRoute(ref, 'healthArchive'), onTap: () => pushRoute(ref, 'healthArchive'),
), ),
], ],
@@ -72,7 +88,7 @@ class ProfilePage extends ConsumerWidget {
icon: Icons.verified_user_rounded, icon: Icons.verified_user_rounded,
label: '隐私保护', label: '隐私保护',
value: '健康数据仅用于个人健康管理', 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 { class _AvatarBadge extends StatelessWidget {
final String name;
final String phone;
final String? avatarUrl; final String? avatarUrl;
const _ProfileHero({ const _AvatarBadge({required this.avatarUrl});
required this.name,
required this.phone,
required this.avatarUrl,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
padding: const EdgeInsets.all(20), width: 50,
height: 50,
padding: const EdgeInsets.all(2),
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: const LinearGradient( color: const Color(0xFFF8FAFC),
begin: Alignment.topLeft, borderRadius: BorderRadius.circular(16),
end: Alignment.bottomRight, border: Border.all(color: AppColors.border, width: 1.1),
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),
), ),
child: ClipRRect( child: ClipRRect(
borderRadius: BorderRadius.circular(25), borderRadius: BorderRadius.circular(14),
child: avatarUrl != null child: avatarUrl != null
? Image.network(avatarUrl!, fit: BoxFit.cover) ? Image.network(avatarUrl!, fit: BoxFit.cover)
: const ColoredBox( : const ColoredBox(
color: Colors.white, color: Colors.white,
child: Icon( child: Icon(
Icons.person_rounded, Icons.person_rounded,
color: Color(0xFF7C3AED), color: Color(0xFF38BDF8),
size: 42, 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( return Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.88), color: Colors.white,
borderRadius: BorderRadius.circular(26), borderRadius: BorderRadius.circular(18),
border: Border.all(color: Colors.white, width: 1.4), border: Border.all(color: AppColors.borderLight),
boxShadow: AppColors.cardShadowLight, boxShadow: AppColors.cardShadowLight,
), ),
child: Column(children: children), child: Column(children: children),
@@ -261,7 +216,7 @@ class _InfoRow extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return InkWell( return InkWell(
onTap: onTap, onTap: onTap,
borderRadius: BorderRadius.circular(18), borderRadius: BorderRadius.circular(14),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 2, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 2, vertical: 12),
child: Row( child: Row(
@@ -275,7 +230,14 @@ class _InfoRow extends StatelessWidget {
end: Alignment.bottomRight, end: Alignment.bottomRight,
colors: colors, 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), child: Icon(icon, color: Colors.white, size: 22),
), ),
@@ -324,6 +286,6 @@ class _SoftDivider extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return const Divider(height: 1, color: Color(0xFFF1F3F8)); return const Divider(height: 1, color: AppColors.divider);
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -10,6 +10,7 @@ import '../../core/app_theme.dart';
import '../../core/api_client.dart' show baseUrl; import '../../core/api_client.dart' show baseUrl;
import '../../core/navigation_provider.dart'; import '../../core/navigation_provider.dart';
import '../../providers/auth_provider.dart'; import '../../providers/auth_provider.dart';
import '../../widgets/enterprise_widgets.dart';
final reportProvider = NotifierProvider<ReportNotifier, ReportState>( final reportProvider = NotifierProvider<ReportNotifier, ReportState>(
ReportNotifier.new, ReportNotifier.new,
@@ -321,13 +322,20 @@ class ReportNotifier extends Notifier<ReportState> {
Future<void> reanalyzeReport(String id) async { Future<void> reanalyzeReport(String id) async {
try { 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; final data = res.data;
if (data is Map && data['code'] != 0) { if (data is Map && data['code'] != 0) {
state = state.copyWith(uploadError: data['message']?.toString() ?? '重新分析失败'); state = state.copyWith(
uploadError: data['message']?.toString() ?? '重新分析失败',
);
return; return;
} }
state = state.copyWith(clearCurrentAnalysis: true, clearUploadError: true); state = state.copyWith(
clearCurrentAnalysis: true,
clearUploadError: true,
);
await loadReports(); await loadReports();
fetchReportDetail(id); fetchReportDetail(id);
} catch (e) { } catch (e) {
@@ -345,6 +353,9 @@ class ReportNotifier extends Notifier<ReportState> {
class ReportListPage extends ConsumerWidget { class ReportListPage extends ConsumerWidget {
const ReportListPage({super.key}); const ReportListPage({super.key});
static const _reportBlue = Color(0xFF8B5CF6);
static const _reportCyan = Color(0xFF38BDF8);
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(reportProvider); final state = ref.watch(reportProvider);
@@ -352,14 +363,17 @@ class ReportListPage extends ConsumerWidget {
if (state.isAnalyzing) { if (state.isAnalyzing) {
return GradientScaffold( return GradientScaffold(
appBar: AppBar( 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('报告管理'), title: const Text('报告管理'),
), ),
body: Center( body: Center(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
const CircularProgressIndicator(color: AppColors.primary), const CircularProgressIndicator(color: _reportBlue),
const SizedBox(height: 16), const SizedBox(height: 16),
Text( Text(
state.uploadingImage == null ? 'AI 正在分析报告...' : '正在上传报告...', state.uploadingImage == null ? 'AI 正在分析报告...' : '正在上传报告...',
@@ -389,6 +403,27 @@ class ReportListPage extends ConsumerWidget {
child: ListView( child: ListView(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
children: [ 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) ...[ if (state.uploadError != null) ...[
_buildUploadError(state.uploadError!), _buildUploadError(state.uploadError!),
const SizedBox(height: 12), const SizedBox(height: 12),
@@ -435,7 +470,7 @@ class ReportListPage extends ConsumerWidget {
Widget _buildUploadButton(BuildContext context, WidgetRef ref) { Widget _buildUploadButton(BuildContext context, WidgetRef ref) {
return FloatingActionButton( return FloatingActionButton(
onPressed: () => _showUploadOptions(context, ref), onPressed: () => _showUploadOptions(context, ref),
backgroundColor: AppColors.primary, backgroundColor: _reportBlue,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: const Icon(Icons.add, size: 28, color: Colors.white), child: const Icon(Icons.add, size: 28, color: Colors.white),
); );
@@ -456,7 +491,7 @@ class ReportListPage extends ConsumerWidget {
ListTile( ListTile(
leading: const Icon( leading: const Icon(
Icons.camera_alt_outlined, Icons.camera_alt_outlined,
color: AppColors.primary, color: _reportBlue,
), ),
title: const Text('拍照上传', style: TextStyle(fontSize: 17)), title: const Text('拍照上传', style: TextStyle(fontSize: 17)),
onTap: () async { onTap: () async {
@@ -474,7 +509,7 @@ class ReportListPage extends ConsumerWidget {
ListTile( ListTile(
leading: const Icon( leading: const Icon(
Icons.photo_library_outlined, Icons.photo_library_outlined,
color: AppColors.primary, color: _reportCyan,
), ),
title: const Text('从相册选择', style: TextStyle(fontSize: 17)), title: const Text('从相册选择', style: TextStyle(fontSize: 17)),
onTap: () async { onTap: () async {
@@ -505,13 +540,13 @@ class ReportListPage extends ConsumerWidget {
width: 100, width: 100,
height: 100, height: 100,
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppColors.cardInner, color: _reportBlue.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(50), borderRadius: BorderRadius.circular(50),
), ),
child: const Icon( child: const Icon(
Icons.description_outlined, Icons.description_outlined,
size: 44, size: 44,
color: AppColors.textHint, color: _reportBlue,
), ),
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
@@ -545,38 +580,34 @@ class ReportListPage extends ConsumerWidget {
margin: const EdgeInsets.only(bottom: 12), margin: const EdgeInsets.only(bottom: 12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFFD5DCE5)), border: Border.all(color: AppColors.border, width: 1.1),
boxShadow: [ boxShadow: [AppTheme.shadowLight],
BoxShadow(
color: const Color(0xFF101828).withValues(alpha: 0.08),
blurRadius: 18,
offset: const Offset(0, 6),
),
],
), ),
child: Material( child: Material(
color: Colors.transparent, color: Colors.transparent,
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(16),
child: InkWell( child: InkWell(
onTap: () => pushRoute(ref, 'aiAnalysis', params: {'id': report.id}), onTap: () => pushRoute(ref, 'aiAnalysis', params: {'id': report.id}),
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(16),
child: Padding( child: Padding(
padding: const EdgeInsets.all(14), padding: const EdgeInsets.all(15),
child: Row( child: Row(
children: [ children: [
Container( Container(
width: 48, width: 48,
height: 48, height: 48,
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppColors.iconBg, color: _reportBlue.withValues(alpha: 0.10),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(14),
border: Border.all(color: AppColors.borderLight), border: Border.all(
color: _reportBlue.withValues(alpha: 0.10),
),
), ),
child: const Icon( child: const Icon(
Icons.description_outlined, Icons.description_outlined,
size: 26, size: 26,
color: AppColors.primary, color: _reportBlue,
), ),
), ),
const SizedBox(width: 14), const SizedBox(width: 14),
@@ -586,9 +617,11 @@ class ReportListPage extends ConsumerWidget {
children: [ children: [
Text( Text(
displayTitle, displayTitle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle( style: const TextStyle(
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w800,
color: AppColors.textPrimary, color: AppColors.textPrimary,
), ),
), ),
@@ -619,17 +652,15 @@ class ReportListPage extends ConsumerWidget {
), ),
_buildStatusBadge(report), _buildStatusBadge(report),
const SizedBox(width: 4), const SizedBox(width: 4),
GestureDetector( IconButton(
onTap: () => _confirmDelete(context, ref, report.id), onPressed: () => _confirmDelete(context, ref, report.id),
child: const Padding( visualDensity: VisualDensity.compact,
padding: EdgeInsets.all(6), icon: const Icon(
child: Icon(
Icons.delete_outline, Icons.delete_outline,
size: 20, size: 20,
color: AppColors.textHint, color: AppColors.textHint,
), ),
), ),
),
], ],
), ),
), ),
@@ -663,26 +694,39 @@ class ReportListPage extends ConsumerWidget {
Widget _buildStatusBadge(ReportItem report) { Widget _buildStatusBadge(ReportItem report) {
final (label, bg, fg) = switch (report.status) { final (label, bg, fg) = switch (report.status) {
_ when report.reviewStatus == 'Reviewed' => ('已审核', AppColors.successLight, AppColors.success), _ when report.reviewStatus == 'Reviewed' => (
_ when report.aiStatus == 'Analyzing' => ('分析中', AppColors.cardInner, AppColors.textHint), '已审核',
_ when report.aiStatus == 'Failed' => ('分析失败', AppColors.error.withValues(alpha: 0.08), AppColors.error), AppColors.successLight,
_ when report.aiStatus == 'Succeeded' => ('待审核', AppColors.warningLight, AppColors.warning), AppColors.success,
_ => ('分析中', AppColors.cardInner, AppColors.textHint), ),
_ 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( return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration( decoration: BoxDecoration(
color: bg, color: bg,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(999),
border: Border.all(color: fg.withValues(alpha: 0.12)),
), ),
child: Text( child: Text(
label, label,
style: TextStyle( style: TextStyle(fontSize: 13, color: fg, fontWeight: FontWeight.w700),
fontSize: 13,
color: fg,
fontWeight: FontWeight.w500,
),
), ),
); );
} }
@@ -719,18 +763,17 @@ class ReportOriginalPage extends StatelessWidget {
final String url; final String url;
final String title; final String title;
const ReportOriginalPage({ const ReportOriginalPage({super.key, required this.url, this.title = '原始报告'});
super.key,
required this.url,
this.title = '原始报告',
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final imageUrl = _absoluteUrl(url); final imageUrl = _absoluteUrl(url);
return GradientScaffold( return GradientScaffold(
appBar: AppBar( 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), title: Text(title),
), ),
body: Center( body: Center(

View File

@@ -55,21 +55,6 @@ class SettingsPage extends ConsumerWidget {
onTap: () => pushRoute(ref, 'notificationPrefs'), onTap: () => pushRoute(ref, 'notificationPrefs'),
), ),
const SizedBox(height: 8), 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( _SettingsTile(
icon: LucideIcons.info, icon: LucideIcons.info,
title: '关于小脉健康', title: '关于小脉健康',

View 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),
);
}
}