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:
MingNian
2026-07-08 21:25:07 +08:00
parent 7a93237069
commit 1c020b8ae5
45 changed files with 3480 additions and 1000 deletions

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

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

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

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

View File

@@ -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),
],
),
);

View File

@@ -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(

View File

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