feat: Cupertino滚轮选择器 + 品牌更名 + UI全面优化
- 日期/时间/次数选择器统一改为底部弹出Cupertino滚轮,无单位标签,双横线选区 - App更名为"小脉健康",副标题"血管病患者的AI健康管理助手" - 健康仪表盘及侧边栏移除体重指标,侧边栏背景改为蓝白渐变 - 健康档案按钮移入功能入口网格,与其他入口样式统一 - 记数据智能体配色改为绿色渐变(#A1FFCE→#69DB8F) - 隐私协议/关于页从Text改为MarkdownBody正确渲染 - 运动计划/饮食记录卡片添加边框和阴影 - 服药次数从chip改为滚轮选择器 - 日期显示格式统一为空格分隔(1999 01 01) - 健康档案页背景改为纯白
This commit is contained in:
@@ -1,7 +1,356 @@
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user