## 后端 - 健康档案: 新增手术状态字段 + EF 迁移; HealthArchiveService 新增查询方法 - 健康记录: HealthRecordService 新增批量/统计方法; 契约扩展 - 用药: 新增 MedicationScheduleStatus 枚举; MedicationService 排班逻辑调整 - 通知: EfUserNotificationPipeline 重构; 新增 EfReminderCatchUpService; 通知管线支持更多场景 - 用户: UserService 账号删除逻辑; 新增 local_account_file_cleanup; EfUserRepository 扩展 - AI: medication_agent_handler 微调; prompt_manager 优化; AiConversationService 上下文处理 - Endpoint: doctor/medication/exercise/health/notification/user 等多接口调整 - BackgroundService: health_record_reminder_service 重构, 提醒补漏逻辑 - 测试: 新增 account_deletion/doctor_endpoint/medication_schedule/medication_update/prompt_manager 测试 ## 前端 - UI 系统: app_theme 大幅重构; app_colors/app_design_tokens/app_module_visuals 调整; 二级页面色彩刷新 - 主页: home_page 背景渐变 + 消息列表提取 _HomeMessages + 通知检查逻辑; chat_messages_view 全面重构 - 用药: medication_list/edit/checkin 三页重构, 新增 medication_ui_logic 抽取 - 通知: notification_prefs_page 重构, 新增 notification_prefs_logic; notification_center 优化 - 设备: device_management 重构, 新增 device_sync_ui_logic; device_scan 优化 - 趋势图: trend_page 大幅重构 - 登录: login_page 重构 - 个人资料: 新增 profile_edit_page; profile_page 优化 - 运动: 新增 exercise/ 目录 + care_plan_ui_logic - 其他: remaining_pages/report_pages/health_drawer/admin/doctor 等多页面调整 - 组件: common_widgets/app_empty_state/app_error_state/app_future_view/app_toast/ai_content 优化 - Provider: chat_provider/consultation_provider/data_providers/auth_provider 调整 - AndroidManifest: 移除多余权限 - 测试: 新增 ai_content/care_plan/home_message/login_flow/medication_checkin/medication_ui/notification_prefs/profile_device/secondary_page/swipe_delete 等大量测试 ## 文档 - 新增 ui-design-system.md 设计系统文档 - 新增 secondary-page-color-refresh 计划 + specs 目录
803 lines
26 KiB
Dart
803 lines
26 KiB
Dart
import 'package:flutter/cupertino.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:shadcn_ui/shadcn_ui.dart';
|
|
import 'app_colors.dart';
|
|
|
|
const _pickerItemStyle = TextStyle(fontSize: 22, fontWeight: FontWeight.w500);
|
|
const _lineColor = Color(0xFFCBD5E1);
|
|
|
|
Widget _wrapPicker(Widget picker) {
|
|
return Stack(
|
|
children: [
|
|
picker,
|
|
Positioned(
|
|
top: 0,
|
|
bottom: 0,
|
|
left: 0,
|
|
right: 0,
|
|
child: IgnorePointer(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Container(height: 1, color: _lineColor),
|
|
const SizedBox(height: 38),
|
|
Container(height: 1, color: _lineColor),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Future<DateTime?> showAppDatePicker(
|
|
BuildContext context, {
|
|
required DateTime initialDate,
|
|
DateTime? firstDate,
|
|
DateTime? lastDate,
|
|
}) {
|
|
final minDate = firstDate ?? DateTime(1900);
|
|
final maxDate = lastDate ?? DateTime(2100);
|
|
var y = initialDate.year.clamp(minDate.year, maxDate.year);
|
|
var m = initialDate.month;
|
|
var d = initialDate.day;
|
|
final yearCtrl = FixedExtentScrollController(initialItem: y - minDate.year);
|
|
final monthCtrl = FixedExtentScrollController(initialItem: m - 1);
|
|
final dayCtrl = FixedExtentScrollController(initialItem: d - 1);
|
|
|
|
int daysInMonth(int year, int month) => DateTime(year, month + 1, 0).day;
|
|
|
|
void clampDay() {
|
|
final maxDay = daysInMonth(y, m);
|
|
if (d > maxDay) d = maxDay;
|
|
}
|
|
|
|
return showCupertinoModalPopup<DateTime>(
|
|
context: context,
|
|
builder: (ctx) => Container(
|
|
height: 320,
|
|
decoration: const BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
CupertinoButton(
|
|
padding: EdgeInsets.zero,
|
|
child: const Text(
|
|
'取消',
|
|
style: TextStyle(fontSize: 16, color: Color(0xFF94A3B8)),
|
|
),
|
|
onPressed: () => Navigator.pop(ctx),
|
|
),
|
|
CupertinoButton(
|
|
padding: EdgeInsets.zero,
|
|
child: const Text(
|
|
'确定',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w600,
|
|
color: AppColors.primary,
|
|
),
|
|
),
|
|
onPressed: () {
|
|
clampDay();
|
|
Navigator.pop(ctx, DateTime(y, m, d));
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Expanded(
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: _wrapPicker(
|
|
CupertinoPicker(
|
|
selectionOverlay: null,
|
|
scrollController: yearCtrl,
|
|
itemExtent: 40,
|
|
onSelectedItemChanged: (i) {
|
|
y = minDate.year + i;
|
|
clampDay();
|
|
final maxDay = daysInMonth(y, m);
|
|
dayCtrl.jumpTo((d - 1).clamp(0, maxDay - 1).toDouble());
|
|
},
|
|
children: [
|
|
for (var i = minDate.year; i <= maxDate.year; i++)
|
|
Center(child: Text('$i', style: _pickerItemStyle)),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
Expanded(
|
|
child: _wrapPicker(
|
|
CupertinoPicker(
|
|
selectionOverlay: null,
|
|
scrollController: monthCtrl,
|
|
itemExtent: 40,
|
|
onSelectedItemChanged: (i) {
|
|
m = i + 1;
|
|
clampDay();
|
|
final maxDay = daysInMonth(y, m);
|
|
dayCtrl.jumpTo((d - 1).clamp(0, maxDay - 1).toDouble());
|
|
},
|
|
children: [
|
|
for (var i = 1; i <= 12; i++)
|
|
Center(
|
|
child: Text(
|
|
i.toString().padLeft(2, '0'),
|
|
style: _pickerItemStyle,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
Expanded(
|
|
child: _wrapPicker(
|
|
CupertinoPicker(
|
|
selectionOverlay: null,
|
|
scrollController: dayCtrl,
|
|
itemExtent: 40,
|
|
onSelectedItemChanged: (i) => d = i + 1,
|
|
children: [
|
|
for (var i = 1; i <= daysInMonth(y, m); i++)
|
|
Center(
|
|
child: Text(
|
|
i.toString().padLeft(2, '0'),
|
|
style: _pickerItemStyle,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<TimeOfDay?> showAppTimePicker(
|
|
BuildContext context, {
|
|
required TimeOfDay initialTime,
|
|
}) {
|
|
var hour = initialTime.hour;
|
|
var minute = initialTime.minute;
|
|
final hourCtrl = FixedExtentScrollController(initialItem: hour);
|
|
final minuteCtrl = FixedExtentScrollController(initialItem: minute);
|
|
|
|
return showCupertinoModalPopup<TimeOfDay>(
|
|
context: context,
|
|
builder: (ctx) => Container(
|
|
height: 320,
|
|
decoration: const BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
CupertinoButton(
|
|
padding: EdgeInsets.zero,
|
|
child: const Text(
|
|
'取消',
|
|
style: TextStyle(fontSize: 16, color: Color(0xFF94A3B8)),
|
|
),
|
|
onPressed: () => Navigator.pop(ctx),
|
|
),
|
|
CupertinoButton(
|
|
padding: EdgeInsets.zero,
|
|
child: const Text(
|
|
'确定',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w600,
|
|
color: AppColors.primary,
|
|
),
|
|
),
|
|
onPressed: () =>
|
|
Navigator.pop(ctx, TimeOfDay(hour: hour, minute: minute)),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Expanded(
|
|
child: Row(
|
|
children: [
|
|
const Spacer(flex: 1),
|
|
Expanded(
|
|
flex: 2,
|
|
child: _wrapPicker(
|
|
CupertinoPicker(
|
|
selectionOverlay: null,
|
|
scrollController: hourCtrl,
|
|
itemExtent: 40,
|
|
onSelectedItemChanged: (i) => hour = i,
|
|
children: [
|
|
for (var i = 0; i <= 23; i++)
|
|
Center(
|
|
child: Text(
|
|
i.toString().padLeft(2, '0'),
|
|
style: _pickerItemStyle,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
Expanded(
|
|
flex: 2,
|
|
child: _wrapPicker(
|
|
CupertinoPicker(
|
|
selectionOverlay: null,
|
|
scrollController: minuteCtrl,
|
|
itemExtent: 40,
|
|
onSelectedItemChanged: (i) => minute = i,
|
|
children: [
|
|
for (var i = 0; i <= 59; i++)
|
|
Center(
|
|
child: Text(
|
|
i.toString().padLeft(2, '0'),
|
|
style: _pickerItemStyle,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
const Spacer(flex: 1),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<DateTime?> showAppDateTimePicker(
|
|
BuildContext context, {
|
|
required DateTime initialDate,
|
|
DateTime? firstDate,
|
|
DateTime? lastDate,
|
|
}) {
|
|
final minDate = firstDate ?? DateTime(1900);
|
|
final maxDate = lastDate ?? DateTime(2100);
|
|
var y = initialDate.year.clamp(minDate.year, maxDate.year);
|
|
var m = initialDate.month;
|
|
var d = initialDate.day;
|
|
var h = initialDate.hour;
|
|
var min = initialDate.minute;
|
|
final yearCtrl = FixedExtentScrollController(initialItem: y - minDate.year);
|
|
final monthCtrl = FixedExtentScrollController(initialItem: m - 1);
|
|
final dayCtrl = FixedExtentScrollController(initialItem: d - 1);
|
|
final hourCtrl = FixedExtentScrollController(initialItem: h);
|
|
final minuteCtrl = FixedExtentScrollController(initialItem: min);
|
|
|
|
int daysInMonth(int year, int month) => DateTime(year, month + 1, 0).day;
|
|
|
|
void clampDay() {
|
|
final maxDay = daysInMonth(y, m);
|
|
if (d > maxDay) d = maxDay;
|
|
}
|
|
|
|
return showCupertinoModalPopup<DateTime>(
|
|
context: context,
|
|
builder: (ctx) => Container(
|
|
height: 320,
|
|
decoration: const BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
CupertinoButton(
|
|
padding: EdgeInsets.zero,
|
|
child: const Text(
|
|
'取消',
|
|
style: TextStyle(fontSize: 16, color: Color(0xFF94A3B8)),
|
|
),
|
|
onPressed: () => Navigator.pop(ctx),
|
|
),
|
|
CupertinoButton(
|
|
padding: EdgeInsets.zero,
|
|
child: const Text(
|
|
'确定',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w600,
|
|
color: AppColors.primary,
|
|
),
|
|
),
|
|
onPressed: () {
|
|
clampDay();
|
|
Navigator.pop(ctx, DateTime(y, m, d, h, min));
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Expanded(
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: _wrapPicker(
|
|
CupertinoPicker(
|
|
selectionOverlay: null,
|
|
scrollController: yearCtrl,
|
|
itemExtent: 40,
|
|
onSelectedItemChanged: (i) {
|
|
y = minDate.year + i;
|
|
clampDay();
|
|
},
|
|
children: [
|
|
for (var i = minDate.year; i <= maxDate.year; i++)
|
|
Center(child: Text('$i', style: _pickerItemStyle)),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
Expanded(
|
|
child: _wrapPicker(
|
|
CupertinoPicker(
|
|
selectionOverlay: null,
|
|
scrollController: monthCtrl,
|
|
itemExtent: 40,
|
|
onSelectedItemChanged: (i) {
|
|
m = i + 1;
|
|
clampDay();
|
|
},
|
|
children: [
|
|
for (var i = 1; i <= 12; i++)
|
|
Center(
|
|
child: Text(
|
|
i.toString().padLeft(2, '0'),
|
|
style: _pickerItemStyle,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
Expanded(
|
|
child: _wrapPicker(
|
|
CupertinoPicker(
|
|
selectionOverlay: null,
|
|
scrollController: dayCtrl,
|
|
itemExtent: 40,
|
|
onSelectedItemChanged: (i) => d = i + 1,
|
|
children: [
|
|
for (var i = 1; i <= daysInMonth(y, m); i++)
|
|
Center(
|
|
child: Text(
|
|
i.toString().padLeft(2, '0'),
|
|
style: _pickerItemStyle,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
Expanded(
|
|
child: _wrapPicker(
|
|
CupertinoPicker(
|
|
selectionOverlay: null,
|
|
scrollController: hourCtrl,
|
|
itemExtent: 40,
|
|
onSelectedItemChanged: (i) => h = i,
|
|
children: [
|
|
for (var i = 0; i <= 23; i++)
|
|
Center(
|
|
child: Text(
|
|
i.toString().padLeft(2, '0'),
|
|
style: _pickerItemStyle,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
Expanded(
|
|
child: _wrapPicker(
|
|
CupertinoPicker(
|
|
selectionOverlay: null,
|
|
scrollController: minuteCtrl,
|
|
itemExtent: 40,
|
|
onSelectedItemChanged: (i) => min = i,
|
|
children: [
|
|
for (var i = 0; i <= 59; i++)
|
|
Center(
|
|
child: Text(
|
|
i.toString().padLeft(2, '0'),
|
|
style: _pickerItemStyle,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<int?> showAppCountPicker(
|
|
BuildContext context, {
|
|
required int initialValue,
|
|
int min = 1,
|
|
int max = 10,
|
|
String label = '',
|
|
}) {
|
|
var selected = initialValue;
|
|
return showCupertinoModalPopup<int>(
|
|
context: context,
|
|
builder: (ctx) => Container(
|
|
height: 320,
|
|
decoration: const BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
CupertinoButton(
|
|
padding: EdgeInsets.zero,
|
|
child: const Text(
|
|
'取消',
|
|
style: TextStyle(fontSize: 16, color: Color(0xFF94A3B8)),
|
|
),
|
|
onPressed: () => Navigator.pop(ctx),
|
|
),
|
|
CupertinoButton(
|
|
padding: EdgeInsets.zero,
|
|
child: const Text(
|
|
'确定',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w600,
|
|
color: AppColors.primary,
|
|
),
|
|
),
|
|
onPressed: () => Navigator.pop(ctx, selected),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Expanded(
|
|
child: _wrapPicker(
|
|
CupertinoPicker(
|
|
selectionOverlay: null,
|
|
scrollController: FixedExtentScrollController(
|
|
initialItem: selected - min,
|
|
),
|
|
itemExtent: 40,
|
|
onSelectedItemChanged: (i) => selected = i + min,
|
|
children: [
|
|
for (var i = min; i <= max; i++)
|
|
Center(
|
|
child: Text(
|
|
label.isEmpty ? '$i' : '$i$label',
|
|
style: _pickerItemStyle,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
class AppBackground extends StatelessWidget {
|
|
final Widget child;
|
|
final bool safeArea;
|
|
|
|
const AppBackground({super.key, required this.child, this.safeArea = false});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final content = safeArea ? SafeArea(child: child) : child;
|
|
|
|
// 全局浅灰白背景,已弃用紫色渐变。
|
|
return DecoratedBox(
|
|
decoration: const BoxDecoration(
|
|
gradient: LinearGradient(
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
colors: [Color(0xFFF0EDFF), Color(0xFFE6ECFF)],
|
|
),
|
|
),
|
|
child: content,
|
|
);
|
|
}
|
|
}
|
|
|
|
class GradientScaffold extends StatelessWidget {
|
|
final PreferredSizeWidget? appBar;
|
|
final Widget? body;
|
|
final Widget? floatingActionButton;
|
|
final Widget? drawer;
|
|
final Widget? bottomNavigationBar;
|
|
final bool extendBody;
|
|
|
|
const GradientScaffold({
|
|
super.key,
|
|
this.appBar,
|
|
this.body,
|
|
this.floatingActionButton,
|
|
this.drawer,
|
|
this.bottomNavigationBar,
|
|
this.extendBody = false,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) => Scaffold(
|
|
// 二级页面用浅中性画布承托白色内容组;首页拥有自己的独立背景实现。
|
|
backgroundColor: AppColors.background,
|
|
appBar: appBar,
|
|
body: ColoredBox(
|
|
color: AppColors.background,
|
|
child: SizedBox.expand(child: body ?? const SizedBox.shrink()),
|
|
),
|
|
floatingActionButton: floatingActionButton,
|
|
drawer: drawer,
|
|
bottomNavigationBar: bottomNavigationBar,
|
|
extendBody: extendBody,
|
|
);
|
|
}
|
|
|
|
class AppTheme {
|
|
AppTheme._();
|
|
|
|
static const Color primary = AppColors.primary;
|
|
static const Color primaryLight = AppColors.primaryLight;
|
|
static const Color primaryDark = AppColors.primaryDark;
|
|
static const Color primaryMid = AppColors.blueMeasure;
|
|
static const Color accent = AppColors.success;
|
|
static const Color info = AppColors.blueMeasure;
|
|
|
|
static const Color bg = AppColors.background;
|
|
static const Color bgSoft = AppColors.backgroundSoft;
|
|
static const Color surface = AppColors.cardBackground;
|
|
static const Color surfaceInner = AppColors.cardInner;
|
|
static const Color text = AppColors.textPrimary;
|
|
static const Color textSub = AppColors.textSecondary;
|
|
static const Color textHint = AppColors.textHint;
|
|
static const Color border = AppColors.border;
|
|
static const Color divider = AppColors.divider;
|
|
|
|
static const Color success = AppColors.success;
|
|
static const Color successLight = AppColors.successLight;
|
|
static const Color error = AppColors.error;
|
|
static const Color errorLight = AppColors.errorLight;
|
|
static const Color warning = AppColors.warning;
|
|
static const Color warningLight = AppColors.warningLight;
|
|
|
|
static const double rXs = 4;
|
|
static const double rSm = 8;
|
|
static const double rMd = 12;
|
|
static const double rLg = 16;
|
|
static const double rXl = 20;
|
|
static const double rPill = 999;
|
|
|
|
static const double sXs = 4;
|
|
static const double sSm = 8;
|
|
static const double sMd = 12;
|
|
static const double sLg = 16;
|
|
static const double sXl = 20;
|
|
static const double sXxl = 24;
|
|
|
|
static BoxShadow get shadowCard => BoxShadow(
|
|
color: const Color(0xFF101828).withValues(alpha: 0.07),
|
|
blurRadius: 22,
|
|
offset: const Offset(0, 10),
|
|
);
|
|
|
|
static BoxShadow get shadowLight => BoxShadow(
|
|
color: const Color(0xFF101828).withValues(alpha: 0.045),
|
|
blurRadius: 14,
|
|
offset: const Offset(0, 6),
|
|
);
|
|
|
|
static BoxShadow get shadowElevated => BoxShadow(
|
|
color: primary.withValues(alpha: 0.12),
|
|
blurRadius: 18,
|
|
offset: const Offset(0, 8),
|
|
);
|
|
|
|
static BoxShadow get shadowButton => BoxShadow(
|
|
color: primary.withValues(alpha: 0.22),
|
|
blurRadius: 16,
|
|
offset: const Offset(0, 8),
|
|
);
|
|
|
|
static const Map<String, Color> agentColors = {
|
|
'default': Color(0xFFF3F0FF),
|
|
'consultation': Color(0xFFEFF5FF),
|
|
'health': Color(0xFFF3F0FF),
|
|
'diet': Color(0xFFFFF7E6),
|
|
'medication': Color(0xFFEFF5FF),
|
|
'report': Color(0xFFF6F3FF),
|
|
'exercise': Color(0xFFEFF8FF),
|
|
};
|
|
|
|
static Color agentLight(String? name) => agentColors[name] ?? primaryLight;
|
|
|
|
static ThemeData get lightTheme => ThemeData(
|
|
useMaterial3: true,
|
|
colorScheme: ColorScheme.fromSeed(
|
|
seedColor: primary,
|
|
primary: primary,
|
|
primaryContainer: primaryLight,
|
|
onPrimary: Colors.white,
|
|
secondary: AppColors.blueMeasure,
|
|
surface: surface,
|
|
brightness: Brightness.light,
|
|
),
|
|
scaffoldBackgroundColor: AppColors.background,
|
|
splashColor: primary.withValues(alpha: 0.06),
|
|
highlightColor: Colors.transparent,
|
|
hoverColor: primaryLight.withValues(alpha: 0.55),
|
|
fontFamily: null,
|
|
|
|
appBarTheme: const AppBarTheme(
|
|
backgroundColor: Colors.white,
|
|
foregroundColor: text,
|
|
elevation: 0,
|
|
centerTitle: true,
|
|
scrolledUnderElevation: 0,
|
|
surfaceTintColor: Colors.transparent,
|
|
titleTextStyle: TextStyle(
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.w800,
|
|
color: text,
|
|
),
|
|
toolbarHeight: 52,
|
|
),
|
|
|
|
cardTheme: CardThemeData(
|
|
color: Colors.white,
|
|
elevation: 0,
|
|
surfaceTintColor: Colors.transparent,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(rLg),
|
|
side: const BorderSide(color: AppColors.borderLight),
|
|
),
|
|
margin: EdgeInsets.zero,
|
|
),
|
|
|
|
inputDecorationTheme: InputDecorationTheme(
|
|
filled: true,
|
|
fillColor: Colors.white,
|
|
contentPadding: const EdgeInsets.symmetric(horizontal: sLg, vertical: 13),
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(rMd),
|
|
borderSide: const BorderSide(color: AppColors.borderLight),
|
|
),
|
|
enabledBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(rMd),
|
|
borderSide: const BorderSide(color: AppColors.borderLight),
|
|
),
|
|
focusedBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(rMd),
|
|
borderSide: const BorderSide(color: AppColors.auraIndigo, width: 1.3),
|
|
),
|
|
errorBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(rMd),
|
|
borderSide: const BorderSide(color: error),
|
|
),
|
|
hintStyle: const TextStyle(color: textHint, fontSize: 15),
|
|
),
|
|
|
|
elevatedButtonTheme: ElevatedButtonThemeData(
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: primary,
|
|
foregroundColor: Colors.white,
|
|
minimumSize: const Size(double.infinity, 50),
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(rLg)),
|
|
textStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700),
|
|
elevation: 0,
|
|
),
|
|
),
|
|
|
|
outlinedButtonTheme: OutlinedButtonThemeData(
|
|
style: OutlinedButton.styleFrom(
|
|
foregroundColor: text,
|
|
side: const BorderSide(color: border),
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(rMd)),
|
|
textStyle: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600),
|
|
),
|
|
),
|
|
|
|
// TextButton 全局默认黑色文字,避免出现浅紫色按钮。
|
|
// 需要红色(如删除)时仍可在使用处单独 styleFrom(foregroundColor: AppColors.error) 覆盖。
|
|
textButtonTheme: TextButtonThemeData(
|
|
style: TextButton.styleFrom(foregroundColor: text),
|
|
),
|
|
|
|
dialogTheme: DialogThemeData(
|
|
backgroundColor: surface,
|
|
surfaceTintColor: Colors.transparent,
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(rLg)),
|
|
),
|
|
|
|
textTheme: const TextTheme(
|
|
headlineLarge: TextStyle(
|
|
fontSize: 24,
|
|
fontWeight: FontWeight.w800,
|
|
color: text,
|
|
),
|
|
titleLarge: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.w700,
|
|
color: text,
|
|
),
|
|
titleMedium: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w600,
|
|
color: text,
|
|
),
|
|
bodyLarge: TextStyle(fontSize: 15, color: text, height: 1.5),
|
|
bodyMedium: TextStyle(fontSize: 13, color: textSub, height: 1.4),
|
|
labelMedium: TextStyle(fontSize: 13, color: textSub),
|
|
labelSmall: TextStyle(fontSize: 12, color: textHint),
|
|
),
|
|
);
|
|
|
|
static ShadThemeData get shadTheme => ShadThemeData(
|
|
brightness: Brightness.light,
|
|
colorScheme: ShadVioletColorScheme.light(
|
|
background: bg,
|
|
foreground: text,
|
|
card: surface,
|
|
cardForeground: text,
|
|
popover: surface,
|
|
popoverForeground: text,
|
|
primary: primary,
|
|
primaryForeground: Colors.white,
|
|
secondary: primaryLight,
|
|
secondaryForeground: primaryDark,
|
|
muted: bgSoft,
|
|
mutedForeground: textSub,
|
|
accent: AppColors.blueMeasure,
|
|
accentForeground: Colors.white,
|
|
destructive: error,
|
|
destructiveForeground: Colors.white,
|
|
border: border,
|
|
input: border,
|
|
ring: primary,
|
|
selection: primaryLight,
|
|
),
|
|
radius: BorderRadius.circular(rMd),
|
|
);
|
|
}
|