feat: UI 系统中心化 + 趋势图重构 + 法律文档 H5 上线
- UI 系统: 新增 design_tokens / module_visuals / app_buttons / app_status_badge / app_toast / ai_content 六个共享模块; 28 个页面接入, SnackBar 全部替换为 AppToast - 趋势图: 直线折线 + 渐变填充 + 阴影; 去网格线; X 轴日+月份分隔; Y 轴整数刻度最多 4 个; 点击数据点/录入记录显示上方浮层, 选中点变实心 - 法律文档 H5: 后端 wwwroot 托管隐私政策/服务协议/关于/个人信息收集清单/第三方 SDK 清单 + 索引页; Program.cs 启用 UseDefaultFiles + UseStaticFiles - 其他: auth_provider 登录流程调整; api_client IP 适配; 主页智能体栏间距优化
This commit is contained in:
87
health_app/lib/widgets/ai_content.dart
Normal file
87
health_app/lib/widgets/ai_content.dart
Normal file
@@ -0,0 +1,87 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
|
||||
import '../core/app_colors.dart';
|
||||
import '../core/app_design_tokens.dart';
|
||||
|
||||
class AiContent {
|
||||
AiContent._();
|
||||
|
||||
static String clean(String text) {
|
||||
var t = text;
|
||||
t = t.replaceAll(RegExp(r'\$\d+'), '');
|
||||
t = t.replaceAll(RegExp(r'<[^>]+>'), '');
|
||||
t = t.replaceAllMapped(RegExp(r'^#{4,}\s', multiLine: true), (_) => '### ');
|
||||
t = t.replaceAll(RegExp(r'\n{3,}'), '\n\n');
|
||||
return t.trim();
|
||||
}
|
||||
|
||||
static String plain(String text) {
|
||||
final cleaned = clean(text);
|
||||
return cleaned
|
||||
.replaceAll(RegExp(r'\*\*(.*?)\*\*'), r'$1')
|
||||
.replaceAll(RegExp(r'__(.*?)__'), r'$1')
|
||||
.replaceAll(RegExp(r'`([^`]+)`'), r'$1')
|
||||
.replaceAll(RegExp(r'^\s*[-*]\s+', multiLine: true), '• ')
|
||||
.trim();
|
||||
}
|
||||
}
|
||||
|
||||
class AiMarkdownView extends StatelessWidget {
|
||||
final String data;
|
||||
final bool selectable;
|
||||
final void Function(String text, String? href, String title)? onTapLink;
|
||||
|
||||
const AiMarkdownView({
|
||||
super.key,
|
||||
required this.data,
|
||||
this.selectable = true,
|
||||
this.onTapLink,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MarkdownBody(
|
||||
data: AiContent.clean(data),
|
||||
selectable: selectable,
|
||||
onTapLink: onTapLink,
|
||||
styleSheet: MarkdownStyleSheet(
|
||||
p: AppTextStyles.chatBody,
|
||||
a: AppTextStyles.chatBody.copyWith(
|
||||
color: AppColors.primary,
|
||||
fontWeight: FontWeight.w800,
|
||||
decoration: TextDecoration.underline,
|
||||
decorationColor: AppColors.primary,
|
||||
),
|
||||
strong: AppTextStyles.chatBody.copyWith(fontWeight: FontWeight.w800),
|
||||
h1: AppTextStyles.chatTitle.copyWith(fontSize: 20),
|
||||
h2: AppTextStyles.chatTitle.copyWith(fontSize: 20),
|
||||
h3: AppTextStyles.chatTitle.copyWith(fontSize: 19),
|
||||
listBullet: AppTextStyles.chatBody.copyWith(
|
||||
fontSize: 21,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
listBulletPadding: const EdgeInsets.only(right: 8),
|
||||
blockquote: AppTextStyles.chatBody.copyWith(color: AppColors.textSecondary),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AiGeneratedNote extends StatelessWidget {
|
||||
const AiGeneratedNote({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => const Padding(
|
||||
padding: EdgeInsets.only(top: 10),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.auto_awesome_rounded, size: 15, color: AppColors.textHint),
|
||||
SizedBox(width: 5),
|
||||
Text('内容由 AI 生成', style: AppTextStyles.aiNote),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
242
health_app/lib/widgets/app_buttons.dart
Normal file
242
health_app/lib/widgets/app_buttons.dart
Normal file
@@ -0,0 +1,242 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../core/app_colors.dart';
|
||||
import '../core/app_design_tokens.dart';
|
||||
|
||||
enum AppButtonVariant { primary, secondary, outline, ghost, danger, gradientOutline }
|
||||
|
||||
class AppButton extends StatelessWidget {
|
||||
final String label;
|
||||
final VoidCallback? onPressed;
|
||||
final IconData? icon;
|
||||
final AppButtonVariant variant;
|
||||
final Color? color;
|
||||
final LinearGradient? gradient;
|
||||
final bool loading;
|
||||
final double height;
|
||||
final bool expand;
|
||||
|
||||
const AppButton({
|
||||
super.key,
|
||||
required this.label,
|
||||
this.onPressed,
|
||||
this.icon,
|
||||
this.variant = AppButtonVariant.primary,
|
||||
this.color,
|
||||
this.gradient,
|
||||
this.loading = false,
|
||||
this.height = 50,
|
||||
this.expand = true,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final enabled = onPressed != null && !loading;
|
||||
final baseColor = color ?? AppColors.primary;
|
||||
final child = _ButtonContent(
|
||||
label: label,
|
||||
icon: icon,
|
||||
loading: loading,
|
||||
foreground: _foreground(baseColor),
|
||||
);
|
||||
|
||||
if (variant == AppButtonVariant.gradientOutline) {
|
||||
return _GradientOutlineButton(
|
||||
label: label,
|
||||
icon: icon,
|
||||
loading: loading,
|
||||
enabled: enabled,
|
||||
height: height,
|
||||
expand: expand,
|
||||
gradient: gradient ?? AppColors.actionOutlineGradient,
|
||||
onPressed: onPressed,
|
||||
);
|
||||
}
|
||||
|
||||
final decoration = _decoration(baseColor, enabled);
|
||||
final width = expand ? double.infinity : null;
|
||||
return SizedBox(
|
||||
height: height,
|
||||
width: width,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: enabled ? onPressed : null,
|
||||
borderRadius: AppRadius.lgBorder,
|
||||
child: Ink(
|
||||
decoration: decoration,
|
||||
child: Center(child: child),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Color _foreground(Color baseColor) {
|
||||
return switch (variant) {
|
||||
AppButtonVariant.primary => Colors.white,
|
||||
AppButtonVariant.secondary => baseColor,
|
||||
AppButtonVariant.outline => baseColor,
|
||||
AppButtonVariant.ghost => baseColor,
|
||||
AppButtonVariant.danger => variant == AppButtonVariant.danger && color == null
|
||||
? AppColors.error
|
||||
: Colors.white,
|
||||
AppButtonVariant.gradientOutline => AppColors.textPrimary,
|
||||
};
|
||||
}
|
||||
|
||||
BoxDecoration _decoration(Color baseColor, bool enabled) {
|
||||
final disabledColor = const Color(0xFFE5E7EB);
|
||||
return switch (variant) {
|
||||
AppButtonVariant.primary => BoxDecoration(
|
||||
color: enabled ? baseColor : disabledColor,
|
||||
borderRadius: AppRadius.lgBorder,
|
||||
boxShadow: enabled ? AppShadows.soft : AppShadows.none,
|
||||
),
|
||||
AppButtonVariant.secondary => BoxDecoration(
|
||||
color: enabled ? baseColor.withValues(alpha: 0.10) : disabledColor,
|
||||
borderRadius: AppRadius.lgBorder,
|
||||
),
|
||||
AppButtonVariant.outline => BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: AppRadius.lgBorder,
|
||||
border: Border.all(color: enabled ? baseColor.withValues(alpha: 0.38) : disabledColor),
|
||||
),
|
||||
AppButtonVariant.ghost => BoxDecoration(
|
||||
color: enabled ? baseColor.withValues(alpha: 0.08) : disabledColor,
|
||||
borderRadius: AppRadius.lgBorder,
|
||||
),
|
||||
AppButtonVariant.danger => BoxDecoration(
|
||||
color: enabled ? (color ?? AppColors.error) : disabledColor,
|
||||
borderRadius: AppRadius.lgBorder,
|
||||
),
|
||||
AppButtonVariant.gradientOutline => const BoxDecoration(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class _ButtonContent extends StatelessWidget {
|
||||
final String label;
|
||||
final IconData? icon;
|
||||
final bool loading;
|
||||
final Color foreground;
|
||||
|
||||
const _ButtonContent({
|
||||
required this.label,
|
||||
required this.icon,
|
||||
required this.loading,
|
||||
required this.foreground,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (loading)
|
||||
SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: CircularProgressIndicator(strokeWidth: 2, color: foreground),
|
||||
)
|
||||
else if (icon != null)
|
||||
Icon(icon, size: 19, color: foreground),
|
||||
if (loading || icon != null) const SizedBox(width: 8),
|
||||
Flexible(
|
||||
child: Text(
|
||||
label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: AppTextStyles.button.copyWith(color: foreground),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _GradientOutlineButton extends StatelessWidget {
|
||||
final String label;
|
||||
final IconData? icon;
|
||||
final bool loading;
|
||||
final bool enabled;
|
||||
final double height;
|
||||
final bool expand;
|
||||
final LinearGradient gradient;
|
||||
final VoidCallback? onPressed;
|
||||
|
||||
const _GradientOutlineButton({
|
||||
required this.label,
|
||||
required this.icon,
|
||||
required this.loading,
|
||||
required this.enabled,
|
||||
required this.height,
|
||||
required this.expand,
|
||||
required this.gradient,
|
||||
required this.onPressed,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: height,
|
||||
width: expand ? double.infinity : null,
|
||||
padding: const EdgeInsets.all(1.5),
|
||||
decoration: BoxDecoration(
|
||||
gradient: enabled ? gradient : null,
|
||||
color: enabled ? null : AppColors.borderLight,
|
||||
borderRadius: AppRadius.lgBorder,
|
||||
boxShadow: enabled ? AppShadows.soft : AppShadows.none,
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(AppRadius.lg - 1.5),
|
||||
child: InkWell(
|
||||
onTap: enabled ? onPressed : null,
|
||||
borderRadius: BorderRadius.circular(AppRadius.lg - 1.5),
|
||||
child: Center(
|
||||
child: _ButtonContent(
|
||||
label: label,
|
||||
icon: icon,
|
||||
loading: loading,
|
||||
foreground: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AppIconTile extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
final Color backgroundColor;
|
||||
final double size;
|
||||
final double iconSize;
|
||||
final BorderRadius? borderRadius;
|
||||
|
||||
const AppIconTile({
|
||||
super.key,
|
||||
required this.icon,
|
||||
required this.color,
|
||||
required this.backgroundColor,
|
||||
this.size = 44,
|
||||
this.iconSize = 22,
|
||||
this.borderRadius,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Container(
|
||||
width: size,
|
||||
height: size,
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
borderRadius: borderRadius ?? AppRadius.mdBorder,
|
||||
border: Border.all(color: color.withValues(alpha: 0.16)),
|
||||
),
|
||||
child: Icon(icon, size: iconSize, color: color),
|
||||
);
|
||||
}
|
||||
|
||||
50
health_app/lib/widgets/app_status_badge.dart
Normal file
50
health_app/lib/widgets/app_status_badge.dart
Normal file
@@ -0,0 +1,50 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../core/app_colors.dart';
|
||||
import '../core/app_design_tokens.dart';
|
||||
|
||||
class AppStatusBadge extends StatelessWidget {
|
||||
final String label;
|
||||
final Color color;
|
||||
final Color? backgroundColor;
|
||||
final IconData? icon;
|
||||
|
||||
const AppStatusBadge({
|
||||
super.key,
|
||||
required this.label,
|
||||
required this.color,
|
||||
this.backgroundColor,
|
||||
this.icon,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Container(
|
||||
constraints: const BoxConstraints(minHeight: 24),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor ?? color.withValues(alpha: 0.10),
|
||||
borderRadius: AppRadius.pillBorder,
|
||||
border: Border.all(color: color.withValues(alpha: 0.18)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (icon != null) ...[
|
||||
Icon(icon, size: 14, color: color),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
Text(label, style: AppTextStyles.tag.copyWith(color: color)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class AppStatusColors {
|
||||
AppStatusColors._();
|
||||
|
||||
static Color done = AppColors.success;
|
||||
static Color warning = AppColors.warning;
|
||||
static Color danger = AppColors.error;
|
||||
static Color info = AppColors.health;
|
||||
}
|
||||
|
||||
86
health_app/lib/widgets/app_toast.dart
Normal file
86
health_app/lib/widgets/app_toast.dart
Normal file
@@ -0,0 +1,86 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
|
||||
import '../core/app_colors.dart';
|
||||
import '../core/app_design_tokens.dart';
|
||||
|
||||
enum AppToastType { success, error, warning, info }
|
||||
|
||||
class AppToast {
|
||||
AppToast._();
|
||||
|
||||
static OverlayEntry? _entry;
|
||||
|
||||
static void show(
|
||||
BuildContext context,
|
||||
String message, {
|
||||
AppToastType type = AppToastType.info,
|
||||
Duration duration = const Duration(milliseconds: 2200),
|
||||
}) {
|
||||
_entry?.remove();
|
||||
final overlay = Overlay.of(context);
|
||||
final visual = _visual(type);
|
||||
_entry = OverlayEntry(
|
||||
builder: (ctx) => Positioned(
|
||||
top: MediaQuery.paddingOf(ctx).top + 62,
|
||||
left: 20,
|
||||
right: 20,
|
||||
child: IgnorePointer(
|
||||
child: Center(
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: Container(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: 42,
|
||||
maxWidth: MediaQuery.sizeOf(ctx).width * 0.82,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: AppRadius.lgBorder,
|
||||
border: Border.all(color: visual.color.withValues(alpha: 0.20)),
|
||||
boxShadow: AppShadows.floating,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(visual.icon, size: 18, color: visual.color),
|
||||
const SizedBox(width: 8),
|
||||
Flexible(
|
||||
child: Text(
|
||||
message,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.textPrimary,
|
||||
height: 1.25,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
overlay.insert(_entry!);
|
||||
Future.delayed(duration, () {
|
||||
_entry?.remove();
|
||||
_entry = null;
|
||||
});
|
||||
}
|
||||
|
||||
static ({IconData icon, Color color}) _visual(AppToastType type) {
|
||||
return switch (type) {
|
||||
AppToastType.success => (icon: LucideIcons.circleCheck, color: AppColors.success),
|
||||
AppToastType.error => (icon: LucideIcons.circleX, color: AppColors.error),
|
||||
AppToastType.warning => (icon: LucideIcons.triangleAlert, color: AppColors.warning),
|
||||
AppToastType.info => (icon: LucideIcons.info, color: AppColors.health),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../core/app_colors.dart';
|
||||
import '../core/app_design_tokens.dart';
|
||||
import '../core/app_module_visuals.dart';
|
||||
import '../models/bp_reading.dart';
|
||||
|
||||
Future<void> showBleSyncDialog(
|
||||
@@ -17,31 +19,35 @@ Future<void> showBleSyncDialog(
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
constraints: const BoxConstraints(maxWidth: 380),
|
||||
padding: const EdgeInsets.fromLTRB(22, 28, 22, 22),
|
||||
padding: const EdgeInsets.fromLTRB(22, 26, 22, 22),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.14),
|
||||
blurRadius: 34,
|
||||
offset: const Offset(0, 18),
|
||||
),
|
||||
],
|
||||
borderRadius: AppRadius.xlBorder,
|
||||
boxShadow: AppShadows.floating,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
'数据已录入',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: AppColors.textPrimary,
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
gradient: AppModuleVisuals.device.gradient,
|
||||
borderRadius: AppRadius.mdBorder,
|
||||
),
|
||||
child: Icon(
|
||||
AppModuleVisuals.device.icon,
|
||||
color: Colors.white,
|
||||
size: 25,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const SizedBox(height: 14),
|
||||
Text(
|
||||
'数据已录入',
|
||||
textAlign: TextAlign.center,
|
||||
style: AppTextStyles.summaryTitle.copyWith(fontSize: 22),
|
||||
),
|
||||
const SizedBox(height: 22),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
@@ -51,7 +57,7 @@ Future<void> showBleSyncDialog(
|
||||
unit: 'mmHg',
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: _MetricCard(
|
||||
label: '舒张压',
|
||||
@@ -62,7 +68,7 @@ Future<void> showBleSyncDialog(
|
||||
],
|
||||
),
|
||||
if (reading.pulse != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: 14),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
@@ -76,7 +82,7 @@ Future<void> showBleSyncDialog(
|
||||
],
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 28),
|
||||
const SizedBox(height: 26),
|
||||
GestureDetector(
|
||||
onTap: () => Navigator.pop(ctx),
|
||||
child: Container(
|
||||
@@ -84,17 +90,13 @@ Future<void> showBleSyncDialog(
|
||||
width: double.infinity,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
gradient: AppColors.doctorGradient,
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
gradient: AppModuleVisuals.device.gradient,
|
||||
borderRadius: AppRadius.lgBorder,
|
||||
boxShadow: AppColors.buttonShadow,
|
||||
),
|
||||
child: const Text(
|
||||
child: Text(
|
||||
'知道了',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
style: AppTextStyles.button.copyWith(color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -121,8 +123,9 @@ class _MetricCard extends StatelessWidget {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF8FAFC),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
color: AppModuleVisuals.device.lightColor,
|
||||
borderRadius: AppRadius.mdBorder,
|
||||
border: Border.all(color: AppModuleVisuals.device.borderColor),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
@@ -136,23 +139,9 @@ class _MetricCard extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
unit,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.textHint,
|
||||
),
|
||||
),
|
||||
Text(unit, style: AppTextStyles.tag),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
Text(label, style: AppTextStyles.tag),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../core/app_colors.dart';
|
||||
import '../core/app_design_tokens.dart';
|
||||
import '../core/app_theme.dart';
|
||||
|
||||
class EnterpriseStat {
|
||||
@@ -35,12 +36,12 @@ class EnterpriseHeader extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
padding: AppSpacing.panel,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderRadius: AppRadius.xlBorder,
|
||||
border: Border.all(color: AppColors.border, width: 1.1),
|
||||
boxShadow: [AppTheme.shadowLight],
|
||||
boxShadow: AppShadows.soft,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -71,23 +72,14 @@ class EnterpriseHeader extends StatelessWidget {
|
||||
title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: AppColors.textPrimary,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
style: AppTextStyles.summaryTitle,
|
||||
),
|
||||
const SizedBox(height: 3),
|
||||
Text(
|
||||
subtitle,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: AppColors.textSecondary,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1.25,
|
||||
),
|
||||
style: AppTextStyles.summarySubtitle,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -143,7 +135,7 @@ class _HeaderStat extends StatelessWidget {
|
||||
accent.withValues(alpha: 0.06),
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderRadius: AppRadius.smBorder,
|
||||
border: Border.all(color: AppColors.borderLight, width: 1.1),
|
||||
),
|
||||
child: Row(
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
import '../core/app_colors.dart';
|
||||
import '../core/app_design_tokens.dart';
|
||||
import '../core/navigation_provider.dart';
|
||||
import '../providers/auth_provider.dart';
|
||||
import '../providers/chat_provider.dart';
|
||||
import '../providers/conversation_history_provider.dart';
|
||||
import 'app_toast.dart';
|
||||
import '../providers/data_providers.dart';
|
||||
import 'drawer_shell.dart';
|
||||
|
||||
@@ -23,7 +26,7 @@ class HealthDrawer extends ConsumerWidget {
|
||||
physics: const BouncingScrollPhysics(),
|
||||
slivers: [
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.fromLTRB(18, 18, 18, 24),
|
||||
padding: const EdgeInsets.fromLTRB(12, 16, 12, 24),
|
||||
sliver: SliverList.list(
|
||||
children: [
|
||||
_AccountHeader(user: auth.user, ref: ref),
|
||||
@@ -305,71 +308,66 @@ class _NavigationSection extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final items = [
|
||||
_NavItem(
|
||||
icon: Icons.folder_shared_rounded,
|
||||
icon: LucideIcons.folderHeart,
|
||||
title: '档案',
|
||||
route: 'healthArchive',
|
||||
colors: const [Color(0xFF93C5FD), Color(0xFF60A5FA)],
|
||||
),
|
||||
_NavItem(
|
||||
icon: Icons.description_rounded,
|
||||
icon: LucideIcons.fileText,
|
||||
title: '报告',
|
||||
route: 'reports',
|
||||
colors: const [Color(0xFFBFDBFE), Color(0xFF93C5FD)],
|
||||
),
|
||||
_NavItem(
|
||||
icon: Icons.medication_rounded,
|
||||
icon: LucideIcons.pill,
|
||||
title: '用药',
|
||||
route: 'medications',
|
||||
colors: const [Color(0xFFDDD6FE), Color(0xFFC4B5FD)],
|
||||
),
|
||||
_NavItem(
|
||||
icon: Icons.restaurant_rounded,
|
||||
icon: LucideIcons.utensils,
|
||||
title: '饮食',
|
||||
route: 'dietRecords',
|
||||
colors: const [Color(0xFFFBCFE8), Color(0xFFF472B6)],
|
||||
),
|
||||
_NavItem(
|
||||
icon: Icons.calendar_month_rounded,
|
||||
icon: LucideIcons.calendarDays,
|
||||
title: '日历',
|
||||
route: 'calendar',
|
||||
colors: const [Color(0xFFC4B5FD), Color(0xFF8B5CF6)],
|
||||
),
|
||||
_NavItem(
|
||||
icon: Icons.event_available_rounded,
|
||||
icon: LucideIcons.calendarCheck,
|
||||
title: '随访',
|
||||
route: 'followups',
|
||||
colors: const [Color(0xFFFBCFE8), Color(0xFFF9A8D4)],
|
||||
),
|
||||
_NavItem(
|
||||
icon: Icons.directions_run_rounded,
|
||||
icon: LucideIcons.footprints,
|
||||
title: '运动',
|
||||
route: 'exercisePlan',
|
||||
colors: const [Color(0xFFA5F3FC), Color(0xFF67E8F9)],
|
||||
),
|
||||
_NavItem(
|
||||
icon: Icons.bluetooth_connected_rounded,
|
||||
title: '蓝牙设备',
|
||||
icon: LucideIcons.bluetooth,
|
||||
title: '设备',
|
||||
route: 'devices',
|
||||
colors: const [Color(0xFF60A5FA), Color(0xFFC4B5FD)],
|
||||
),
|
||||
];
|
||||
|
||||
return _Panel(
|
||||
title: '功能入口',
|
||||
backgroundGradient: const LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [Color(0xFAFFFFFF), Color(0xF5EFF6FF)],
|
||||
),
|
||||
return _LightSection(
|
||||
title: '常用功能',
|
||||
child: GridView.builder(
|
||||
itemCount: items.length,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 3,
|
||||
mainAxisExtent: 98,
|
||||
mainAxisSpacing: 10,
|
||||
crossAxisSpacing: 10,
|
||||
crossAxisCount: 2,
|
||||
mainAxisExtent: 48,
|
||||
mainAxisSpacing: 8,
|
||||
crossAxisSpacing: 8,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
final item = items[index];
|
||||
@@ -398,14 +396,14 @@ class _NavTile extends StatelessWidget {
|
||||
};
|
||||
|
||||
List<Color> get _colors => switch (item.route) {
|
||||
'healthArchive' => const [Color(0xFF93C5FD), Color(0xFF60A5FA)],
|
||||
'reports' => const [Color(0xFF60A5FA), Color(0xFF2563EB)],
|
||||
'medications' => const [Color(0xFFA78BFA), Color(0xFF7C3AED)],
|
||||
'dietRecords' => const [Color(0xFFFBCFE8), Color(0xFFF472B6)],
|
||||
'calendar' => const [Color(0xFFC4B5FD), Color(0xFF8B5CF6)],
|
||||
'followups' => const [Color(0xFFF472B6), Color(0xFFDB2777)],
|
||||
'exercisePlan' => const [Color(0xFF7DD3FC), Color(0xFF60A5FA)],
|
||||
'devices' => const [Color(0xFF60A5FA), Color(0xFFC4B5FD)],
|
||||
'healthArchive' => const [AppColors.healthLight, AppColors.health],
|
||||
'reports' => const [AppColors.reportLight, AppColors.report],
|
||||
'medications' => const [AppColors.medicationLight, AppColors.medication],
|
||||
'dietRecords' => const [AppColors.dietLight, AppColors.diet],
|
||||
'calendar' => const [AppColors.calendarLight, AppColors.calendar],
|
||||
'followups' => const [AppColors.followupLight, AppColors.followup],
|
||||
'exercisePlan' => const [AppColors.exerciseLight, AppColors.exercise],
|
||||
'devices' => const [AppColors.deviceLight, AppColors.device],
|
||||
_ => item.colors,
|
||||
};
|
||||
|
||||
@@ -413,39 +411,39 @@ class _NavTile extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
borderRadius: AppRadius.mdBorder,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: AppColors.borderLight, width: 1.1),
|
||||
boxShadow: AppColors.cardShadowLight,
|
||||
color: Colors.white.withValues(alpha: 0.54),
|
||||
borderRadius: AppRadius.mdBorder,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 44,
|
||||
height: 44,
|
||||
width: 30,
|
||||
height: 30,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: _colors,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderRadius: AppRadius.smBorder,
|
||||
),
|
||||
child: Icon(item.icon, color: Colors.white, size: 24),
|
||||
child: Icon(item.icon, color: Colors.white, size: 19),
|
||||
),
|
||||
const SizedBox(height: 9),
|
||||
Text(
|
||||
_title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: AppColors.textPrimary,
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -455,6 +453,39 @@ class _NavTile extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _LightSection extends StatelessWidget {
|
||||
final String title;
|
||||
final Widget child;
|
||||
|
||||
const _LightSection({required this.title, required this.child});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 2),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
child,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
class _Panel extends StatelessWidget {
|
||||
final String title;
|
||||
final Widget child;
|
||||
@@ -484,18 +515,12 @@ class _Panel extends StatelessWidget {
|
||||
const Color(0xFFF8F4FF).withValues(alpha: 0.84),
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
borderRadius: AppRadius.xlBorder,
|
||||
border: Border.all(
|
||||
color: Colors.white.withValues(alpha: 0.82),
|
||||
width: 1.2,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: const Color(0xFF6D5DF6).withValues(alpha: 0.08),
|
||||
blurRadius: 22,
|
||||
offset: const Offset(0, 12),
|
||||
),
|
||||
],
|
||||
boxShadow: AppShadows.soft,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -611,8 +636,6 @@ class _NavItem {
|
||||
required this.colors,
|
||||
});
|
||||
}
|
||||
|
||||
/// 侧边栏底部:最近 5 条对话历史 + 查看全部 + 清空。
|
||||
class _HistorySection extends ConsumerWidget {
|
||||
final WidgetRef ref;
|
||||
const _HistorySection({required this.ref});
|
||||
@@ -668,7 +691,7 @@ class _HistorySection extends ConsumerWidget {
|
||||
item: preview[i],
|
||||
isLast: i == preview.length - 1,
|
||||
onTap: () async {
|
||||
Navigator.of(context).maybePop(); // 关侧边栏
|
||||
Navigator.of(context).maybePop(); // 关闭侧边栏
|
||||
await ref
|
||||
.read(chatProvider.notifier)
|
||||
.loadConversation(preview[i].id);
|
||||
@@ -779,11 +802,10 @@ class _HistoryActions extends StatelessWidget {
|
||||
await ref.read(conversationHistoryProvider.notifier).clearAll();
|
||||
} catch (_) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('清空失败,请稍后重试'),
|
||||
backgroundColor: AppColors.error,
|
||||
),
|
||||
AppToast.show(
|
||||
context,
|
||||
'清空失败,请稍后重试',
|
||||
type: AppToastType.error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user