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

@@ -8,6 +8,7 @@ import '../../providers/auth_provider.dart';
import '../../providers/data_providers.dart';
import '../../widgets/app_empty_state.dart';
import '../../widgets/app_error_state.dart';
import '../../widgets/app_toast.dart';
class MedicationCheckInPage extends ConsumerStatefulWidget {
final String? medId;
@@ -66,11 +67,10 @@ class _MedicationCheckInPageState extends ConsumerState<MedicationCheckInPage> {
} catch (e) {
debugPrint('[MedCheckIn] 打卡失败: $e');
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('打卡失败,请稍后重试'),
backgroundColor: AppTheme.error,
),
AppToast.show(
context,
'打卡失败,请稍后重试',
type: AppToastType.error,
);
}
} finally {

View File

@@ -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) {

View File

@@ -1,11 +1,14 @@
import 'package:flutter/material.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_empty_state.dart';
import '../../widgets/app_future_view.dart';
import '../../widgets/app_status_badge.dart';
import '../../widgets/common_widgets.dart';
import '../../widgets/enterprise_widgets.dart';
@@ -16,8 +19,7 @@ class MedicationListPage extends ConsumerStatefulWidget {
}
class _MedicationListPageState extends ConsumerState<MedicationListPage> {
static const _medBlue = Color(0xFF60A5FA);
static const _medCyan = Color(0xFF8B5CF6);
static const _medVisual = AppModuleVisuals.medication;
Future<List<Map<String, dynamic>>>? _future;
@override
@@ -50,7 +52,7 @@ class _MedicationListPageState extends ConsumerState<MedicationListPage> {
),
floatingActionButton: FloatingActionButton(
onPressed: () => pushRoute(ref, 'medicationEdit'),
backgroundColor: _medBlue,
backgroundColor: _medVisual.color,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: const Icon(Icons.add, size: 28, color: Colors.white),
),
@@ -60,8 +62,8 @@ class _MedicationListPageState extends ConsumerState<MedicationListPage> {
errorTitle: '用药信息加载失败',
onData: (ctx, list) {
if (list.isEmpty) {
return const AppEmptyState(
icon: Icons.medication_outlined,
return AppEmptyState(
icon: _medVisual.icon,
title: '暂无用药',
subtitle: '点击右下角➕添加药品',
);
@@ -77,9 +79,9 @@ class _MedicationListPageState extends ConsumerState<MedicationListPage> {
EnterpriseHeader(
title: '今日用药概览',
subtitle: '集中管理服药计划、剂量和每日打卡状态',
icon: Icons.medication_outlined,
color: _medBlue,
accent: _medCyan,
icon: _medVisual.icon,
color: _medVisual.color,
accent: AppColors.primary,
showIcon: false,
stats: [
EnterpriseStat(
@@ -129,17 +131,13 @@ class _MedicationListPageState extends ConsumerState<MedicationListPage> {
height: 44,
decoration: BoxDecoration(
gradient: isActive
? const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [_medBlue, _medCyan],
)
? _medVisual.gradient
: AppColors.surfaceGradient,
borderRadius: BorderRadius.circular(14),
borderRadius: AppRadius.mdBorder,
border: Border.all(color: AppColors.borderLight),
),
child: Icon(
Icons.medication_outlined,
_medVisual.icon,
size: 25,
color: isActive ? Colors.white : AppTheme.textSub,
),
@@ -153,9 +151,7 @@ class _MedicationListPageState extends ConsumerState<MedicationListPage> {
children: [
Text(
m['name']?.toString() ?? '',
style: TextStyle(
fontSize: 19,
fontWeight: FontWeight.w600,
style: AppTextStyles.listTitle.copyWith(
color: isActive
? AppTheme.text
: AppTheme.textSub,
@@ -163,24 +159,9 @@ class _MedicationListPageState extends ConsumerState<MedicationListPage> {
),
if (!isActive) ...[
const SizedBox(width: 6),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 1,
),
decoration: BoxDecoration(
color: _medBlue.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(
999,
),
),
child: Text(
'已停',
style: TextStyle(
fontSize: 13,
color: _medBlue,
),
),
AppStatusBadge(
label: '已停',
color: _medVisual.color,
),
],
],
@@ -188,8 +169,7 @@ class _MedicationListPageState extends ConsumerState<MedicationListPage> {
const SizedBox(height: 2),
Text(
'${m['dosage'] ?? ''} $times',
style: TextStyle(
fontSize: 16,
style: AppTextStyles.listSubtitle.copyWith(
color: AppTheme.textSub,
),
),