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:
@@ -1,18 +1,26 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../core/app_colors.dart';
|
||||
import '../../core/app_theme.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../core/app_colors.dart';
|
||||
import '../../core/app_design_tokens.dart';
|
||||
import '../../core/app_module_visuals.dart';
|
||||
import '../../core/app_theme.dart';
|
||||
import '../../core/navigation_provider.dart';
|
||||
import '../../providers/data_providers.dart';
|
||||
import '../../widgets/app_toast.dart';
|
||||
|
||||
class MedicationEditPage extends ConsumerStatefulWidget {
|
||||
final String? id;
|
||||
|
||||
const MedicationEditPage({super.key, this.id});
|
||||
|
||||
@override
|
||||
ConsumerState<MedicationEditPage> createState() => _MedicationEditPageState();
|
||||
}
|
||||
|
||||
class _MedicationEditPageState extends ConsumerState<MedicationEditPage> {
|
||||
static const _visual = AppModuleVisuals.medication;
|
||||
|
||||
final _nameCtrl = TextEditingController();
|
||||
final _dosageCtrl = TextEditingController();
|
||||
final _notesCtrl = TextEditingController();
|
||||
@@ -74,9 +82,7 @@ class _MedicationEditPageState extends ConsumerState<MedicationEditPage> {
|
||||
Future<void> _save() async {
|
||||
final name = _nameCtrl.text.trim();
|
||||
if (name.isEmpty) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('请输入药品名称')));
|
||||
AppToast.show(context, '请输入药品名称', type: AppToastType.warning);
|
||||
return;
|
||||
}
|
||||
setState(() => _loading = true);
|
||||
@@ -107,17 +113,10 @@ class _MedicationEditPageState extends ConsumerState<MedicationEditPage> {
|
||||
}
|
||||
ref.invalidate(medicationListProvider);
|
||||
ref.invalidate(medicationReminderProvider);
|
||||
if (mounted) {
|
||||
popRoute(ref);
|
||||
}
|
||||
if (mounted) popRoute(ref);
|
||||
} catch (_) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('保存失败'),
|
||||
backgroundColor: AppTheme.error,
|
||||
),
|
||||
);
|
||||
AppToast.show(context, '保存失败', type: AppToastType.error);
|
||||
}
|
||||
}
|
||||
if (mounted) setState(() => _loading = false);
|
||||
@@ -148,23 +147,22 @@ class _MedicationEditPageState extends ConsumerState<MedicationEditPage> {
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
// 名称+剂量 一行
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: _field('药品名称', _nameCtrl, hint: '如: 阿司匹林'),
|
||||
child: _field('药品名称', _nameCtrl, hint: '如:阿司匹林'),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: _field('剂量', _dosageCtrl, hint: '如: 100mg'),
|
||||
child: _field('剂量', _dosageCtrl, hint: '如:100mg'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// 频率选择
|
||||
_label('每日服药次数'), const SizedBox(height: 8),
|
||||
_label('每日服药次数'),
|
||||
const SizedBox(height: 8),
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
final n = await showAppCountPicker(
|
||||
@@ -172,27 +170,17 @@ class _MedicationEditPageState extends ConsumerState<MedicationEditPage> {
|
||||
initialValue: _timesPerDay,
|
||||
min: 1,
|
||||
max: 4,
|
||||
label: ' 次',
|
||||
label: '次',
|
||||
);
|
||||
if (n != null) _updateTimes(n);
|
||||
},
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.surface,
|
||||
borderRadius: BorderRadius.circular(AppTheme.rMd),
|
||||
border: Border.all(color: AppColors.border),
|
||||
),
|
||||
child: Text(
|
||||
'$_timesPerDay 次/天',
|
||||
style: const TextStyle(fontSize: 18),
|
||||
),
|
||||
child: _PickerBox(
|
||||
child: Text('$_timesPerDay 次/天', style: AppTextStyles.formValue),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// 时间选择
|
||||
_label('服药时间'), const SizedBox(height: 8),
|
||||
_label('服药时间'),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
@@ -213,23 +201,18 @@ class _MedicationEditPageState extends ConsumerState<MedicationEditPage> {
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: AppColors.border, width: 1),
|
||||
borderRadius: AppRadius.mdBorder,
|
||||
border: Border.all(color: _visual.borderColor, width: 1),
|
||||
),
|
||||
child: Text(
|
||||
'${_times[i].hour.toString().padLeft(2, '0')}:${_times[i].minute.toString().padLeft(2, '0')}',
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
color: AppColors.textPrimary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
style: AppTextStyles.formValue,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// 开始+结束 一行
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
@@ -242,7 +225,7 @@ class _MedicationEditPageState extends ConsumerState<MedicationEditPage> {
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _dateFieldOpt(
|
||||
'结束日期(可选)',
|
||||
'结束日期(可选)',
|
||||
_end,
|
||||
(d) => setState(() => _end = d),
|
||||
),
|
||||
@@ -250,7 +233,7 @@ class _MedicationEditPageState extends ConsumerState<MedicationEditPage> {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_field('备注', _notesCtrl, hint: '如: 饭后服用、睡前'),
|
||||
_field('备注', _notesCtrl, hint: '如:饭后服用、睡前'),
|
||||
const SizedBox(height: 32),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
@@ -258,20 +241,12 @@ class _MedicationEditPageState extends ConsumerState<MedicationEditPage> {
|
||||
onPressed: _loading ? null : _save,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: AppColors.primary,
|
||||
side: const BorderSide(color: AppColors.primary, width: 1.5),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
foregroundColor: _visual.color,
|
||||
side: BorderSide(color: _visual.color, width: 1.5),
|
||||
shape: RoundedRectangleBorder(borderRadius: AppRadius.lgBorder),
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
),
|
||||
child: Text(
|
||||
_loading ? '保存中...' : '保存',
|
||||
style: const TextStyle(
|
||||
fontSize: 19,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
child: Text(_loading ? '保存中...' : '保存', style: AppTextStyles.button),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
@@ -280,10 +255,8 @@ class _MedicationEditPageState extends ConsumerState<MedicationEditPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _label(String text) => Text(
|
||||
text,
|
||||
style: TextStyle(fontSize: 17, color: AppColors.textSecondary),
|
||||
);
|
||||
Widget _label(String text) => Text(text, style: AppTextStyles.formLabel);
|
||||
|
||||
Widget _field(String label, TextEditingController ctrl, {String? hint}) =>
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -296,27 +269,19 @@ class _MedicationEditPageState extends ConsumerState<MedicationEditPage> {
|
||||
hintText: hint,
|
||||
filled: true,
|
||||
fillColor: AppTheme.surface,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(AppTheme.rMd),
|
||||
borderSide: const BorderSide(color: AppColors.border),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(AppTheme.rMd),
|
||||
borderSide: const BorderSide(color: AppColors.border),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(AppTheme.rMd),
|
||||
borderSide: const BorderSide(color: AppColors.primary),
|
||||
),
|
||||
border: _inputBorder(AppColors.border),
|
||||
enabledBorder: _inputBorder(AppColors.border),
|
||||
focusedBorder: _inputBorder(_visual.color),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
style: const TextStyle(fontSize: 19),
|
||||
style: AppTextStyles.formValue,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
Widget _dateField(String label, DateTime val, ValueChanged<DateTime> cb) =>
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -333,22 +298,13 @@ class _MedicationEditPageState extends ConsumerState<MedicationEditPage> {
|
||||
);
|
||||
if (d != null) cb(d);
|
||||
},
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.surface,
|
||||
borderRadius: BorderRadius.circular(AppTheme.rMd),
|
||||
border: Border.all(color: AppColors.border),
|
||||
),
|
||||
child: Text(
|
||||
_displayDate(val),
|
||||
style: const TextStyle(fontSize: 18),
|
||||
),
|
||||
child: _PickerBox(
|
||||
child: Text(_displayDate(val), style: AppTextStyles.formValue),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
Widget _dateFieldOpt(
|
||||
String label,
|
||||
DateTime? val,
|
||||
@@ -368,24 +324,17 @@ class _MedicationEditPageState extends ConsumerState<MedicationEditPage> {
|
||||
);
|
||||
if (d != null) cb(d);
|
||||
},
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.surface,
|
||||
borderRadius: BorderRadius.circular(AppTheme.rMd),
|
||||
border: Border.all(color: AppColors.border),
|
||||
),
|
||||
child: _PickerBox(
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
val != null ? _displayDate(val) : '不设置',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
color: val != null ? null : AppTheme.textHint,
|
||||
Expanded(
|
||||
child: Text(
|
||||
val != null ? _displayDate(val) : '不设置',
|
||||
style: AppTextStyles.formValue.copyWith(
|
||||
color: val != null ? null : AppTheme.textHint,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (val != null) const Spacer(),
|
||||
if (val != null)
|
||||
GestureDetector(
|
||||
onTap: () => cb(null),
|
||||
@@ -401,6 +350,31 @@ class _MedicationEditPageState extends ConsumerState<MedicationEditPage> {
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
OutlineInputBorder _inputBorder(Color color) => OutlineInputBorder(
|
||||
borderRadius: AppRadius.mdBorder,
|
||||
borderSide: BorderSide(color: color),
|
||||
);
|
||||
}
|
||||
|
||||
class _PickerBox extends StatelessWidget {
|
||||
final Widget child;
|
||||
|
||||
const _PickerBox({required this.child});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.surface,
|
||||
borderRadius: AppRadius.mdBorder,
|
||||
border: Border.all(color: AppColors.border),
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _displayDate(DateTime date) {
|
||||
|
||||
Reference in New Issue
Block a user