feat: UI 清新风全面改造 — 朝露主题 + shadcn_ui + 全局字号放大

- 全新"朝露"主题:深青绿主色(#2D6A4F),暖白底,完整设计Token
- 引入 shadcn_ui 组件库,MaterialApp 注入 ShadTheme
- 新增 4 个公共组件:AppCard、AppMenuItem、AppEmptyState、AppTabChip
- 全面消除硬编码颜色,统一使用 AppTheme Token
- 全局字号 +3~4pt,图标等比放大 +3px
- home/medication/profile/settings/login 等核心页面重写
- 路由和功能逻辑零改动
This commit is contained in:
MingNian
2026-06-08 18:06:18 +08:00
parent 16d1d3d305
commit 58af5f6d5b
28 changed files with 1616 additions and 812 deletions

View File

@@ -0,0 +1,46 @@
import 'package:flutter/material.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import '../core/app_theme.dart';
/// 统一样式的清新卡片
class AppCard extends StatelessWidget {
final Widget child;
final EdgeInsetsGeometry? padding;
final EdgeInsetsGeometry? margin;
final VoidCallback? onTap;
final Color? backgroundColor;
final BorderRadius? borderRadius;
const AppCard({
super.key,
required this.child,
this.padding = const EdgeInsets.all(AppTheme.sLg),
this.margin,
this.onTap,
this.backgroundColor,
this.borderRadius,
});
@override
Widget build(BuildContext context) {
final theme = ShadTheme.of(context);
final card = Container(
margin: margin ?? const EdgeInsets.symmetric(horizontal: AppTheme.sLg, vertical: AppTheme.sSm),
padding: padding,
decoration: BoxDecoration(
color: backgroundColor ?? theme.colorScheme.card,
borderRadius: borderRadius ?? BorderRadius.circular(AppTheme.rMd),
boxShadow: [AppTheme.shadowLight],
),
child: child,
);
if (onTap != null) {
return GestureDetector(
onTap: onTap,
child: card,
);
}
return card;
}
}

View File

@@ -0,0 +1,52 @@
import 'package:flutter/material.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
/// 统一空状态占位组件
class AppEmptyState extends StatelessWidget {
final IconData icon;
final String title;
final String? subtitle;
final Widget? action;
const AppEmptyState({
super.key,
required this.icon,
required this.title,
this.subtitle,
this.action,
});
@override
Widget build(BuildContext context) {
final theme = ShadTheme.of(context);
return Center(
child: Padding(
padding: const EdgeInsets.all(48),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: theme.colorScheme.muted,
borderRadius: BorderRadius.circular(40),
),
child: Icon(icon, size: 36, color: theme.colorScheme.mutedForeground),
),
const SizedBox(height: 20),
Text(title, style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600, color: theme.colorScheme.foreground)),
if (subtitle != null) ...[
const SizedBox(height: 6),
Text(subtitle!, style: TextStyle(fontSize: 17, color: theme.colorScheme.mutedForeground), textAlign: TextAlign.center),
],
if (action != null) ...[
const SizedBox(height: 20),
action!,
],
],
),
),
);
}
}

View File

@@ -0,0 +1,67 @@
import 'package:flutter/material.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import '../core/app_theme.dart';
/// 统一菜单项profile_page 和 settings_pages 都共用这个
class AppMenuItem extends StatelessWidget {
final IconData icon;
final String title;
final String? subtitle;
final String? trailing;
final VoidCallback? onTap;
const AppMenuItem({
super.key,
required this.icon,
required this.title,
this.subtitle,
this.trailing,
this.onTap,
});
@override
Widget build(BuildContext context) {
final theme = ShadTheme.of(context);
return GestureDetector(
onTap: onTap,
behavior: HitTestBehavior.opaque,
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 24, vertical: 2),
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 14),
decoration: BoxDecoration(
color: theme.colorScheme.card,
borderRadius: BorderRadius.circular(AppTheme.rSm),
),
child: Row(children: [
Container(
width: 38,
height: 38,
decoration: BoxDecoration(
color: AppTheme.primaryLight,
borderRadius: BorderRadius.circular(AppTheme.rSm),
),
child: Icon(icon, size: 22, color: AppTheme.primary),
),
const SizedBox(width: AppTheme.sLg),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: TextStyle(fontSize: 19, fontWeight: FontWeight.w500, color: theme.colorScheme.foreground)),
if (subtitle != null && subtitle!.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 2),
child: Text(subtitle!, style: TextStyle(fontSize: 16, color: theme.colorScheme.mutedForeground)),
),
],
),
),
if (trailing != null && trailing!.isNotEmpty)
Text(trailing!, style: TextStyle(fontSize: 17, color: theme.colorScheme.mutedForeground)),
const SizedBox(width: 4),
Icon(LucideIcons.chevronRight, size: 21, color: theme.colorScheme.mutedForeground),
]),
),
);
}
}

View File

@@ -0,0 +1,39 @@
import 'package:flutter/material.dart';
import '../core/app_theme.dart';
/// 统一样式的标签/筛选 Chip
class AppTabChip extends StatelessWidget {
final String label;
final bool selected;
final VoidCallback onTap;
const AppTabChip({
super.key,
required this.label,
required this.selected,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
margin: const EdgeInsets.only(right: AppTheme.sSm),
padding: const EdgeInsets.symmetric(horizontal: AppTheme.sLg, vertical: AppTheme.sSm),
decoration: BoxDecoration(
color: selected ? AppTheme.primary : AppTheme.primaryLight,
borderRadius: BorderRadius.circular(AppTheme.rPill),
),
child: Text(
label,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: selected ? Colors.white : AppTheme.primary,
),
),
),
);
}
}

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import '../core/app_theme.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../core/navigation_provider.dart';
import '../providers/auth_provider.dart';
@@ -46,12 +47,12 @@ class HealthDrawer extends ConsumerWidget {
Expanded(child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(user?.name ?? '未设置昵称', style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w700, color: Colors.white)),
Text(user?.name ?? '未设置昵称', style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w700, color: Colors.white)),
const SizedBox(height: 2),
Text(user?.phone ?? '未登录', style: TextStyle(fontSize: 12, color: Colors.white70)),
Text(user?.phone ?? '未登录', style: TextStyle(fontSize: 15, color: Colors.white70)),
],
)),
Icon(Icons.chevron_right, size: 18, color: Colors.white54),
Icon(Icons.chevron_right, size: 21, color: Colors.white54),
]),
),
),
@@ -72,19 +73,19 @@ class HealthDrawer extends ConsumerWidget {
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: const Color(0xFF6C5CE7).withAlpha(15),
color: AppTheme.primary.withAlpha(15),
borderRadius: BorderRadius.circular(6),
),
child: const Row(mainAxisSize: MainAxisSize.min, children: [
Icon(Icons.monitor_heart_rounded, size: 13, color: Color(0xFF6C5CE7)),
Icon(Icons.monitor_heart_rounded, size: 16, color: AppTheme.primary),
SizedBox(width: 4),
Text('健康概览', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: Color(0xFF6C5CE7))),
Text('健康概览', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: AppTheme.primary)),
]),
),
const Spacer(),
GestureDetector(
onTap: () => pushRoute(ref, 'trend'),
child: const Padding(padding: EdgeInsets.all(4), child: Text('详情', style: TextStyle(fontSize: 11, color: Color(0xFF888888)))),
child: const Padding(padding: EdgeInsets.all(4), child: Text('详情', style: TextStyle(fontSize: 14, color: Color(0xFF888888)))),
),
]),
),
@@ -99,7 +100,7 @@ class HealthDrawer extends ConsumerWidget {
_MiniMetric(icon: Icons.monitor_weight_outlined, label: '体重', value: _metricVal(data['Weight']), onTap: () => pushRoute(ref, 'trend', params: {'type': 'weight'})),
]),
),
loading: () => const Padding(padding: EdgeInsets.symmetric(vertical: 20), child: Center(child: SizedBox(width: 22, height: 22, child: CircularProgressIndicator(strokeWidth: 2, color: Color(0xFF6C5CE7))))),
loading: () => const Padding(padding: EdgeInsets.symmetric(vertical: 20), child: Center(child: SizedBox(width: 22, height: 22, child: CircularProgressIndicator(strokeWidth: 2, color: AppTheme.primary)))),
error: (_, __) => Padding(
padding: const EdgeInsets.fromLTRB(12, 4, 12, 14),
child: Row(children: [
@@ -134,9 +135,9 @@ class HealthDrawer extends ConsumerWidget {
borderRadius: BorderRadius.circular(6),
),
child: const Row(mainAxisSize: MainAxisSize.min, children: [
Icon(Icons.apps_rounded, size: 13, color: Color(0xFFF0A060)),
Icon(Icons.apps_rounded, size: 16, color: Color(0xFFF0A060)),
SizedBox(width: 4),
Text('功能', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: Color(0xFFF0A060))),
Text('功能', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: Color(0xFFF0A060))),
]),
),
]),
@@ -179,11 +180,11 @@ class HealthDrawer extends ConsumerWidget {
color: const Color(0xFFEEEEEE),
borderRadius: BorderRadius.circular(10),
),
child: const Icon(Icons.settings_outlined, size: 18, color: Color(0xFF666666)),
child: const Icon(Icons.settings_outlined, size: 21, color: Color(0xFF666666)),
),
const SizedBox(width: 12),
const Expanded(child: Text('设置', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: Color(0xFF333333)))),
const Icon(Icons.chevron_right, size: 16, color: Color(0xFFCCCCCC)),
const Expanded(child: Text('设置', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w500, color: Color(0xFF333333)))),
const Icon(Icons.chevron_right, size: 19, color: Color(0xFFCCCCCC)),
]),
),
),
@@ -269,7 +270,7 @@ class _MiniMetric extends StatelessWidget {
final VoidCallback? onTap;
const _MiniMetric({required this.icon, required this.label, required this.value, this.onTap});
static const _c = Color(0xFF6C5CE7);
static const _c = AppTheme.primary;
@override
Widget build(BuildContext context) {
@@ -284,11 +285,11 @@ class _MiniMetric extends StatelessWidget {
borderRadius: BorderRadius.circular(12),
),
child: Column(mainAxisSize: MainAxisSize.min, children: [
Icon(icon, size: 18, color: _c),
Icon(icon, size: 21, color: _c),
const SizedBox(height: 6),
Text(value, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w700, color: Color(0xFF1A1A1A)), maxLines: 1, overflow: TextOverflow.ellipsis),
Text(value, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: Color(0xFF1A1A1A)), maxLines: 1, overflow: TextOverflow.ellipsis),
const SizedBox(height: 2),
Text(label, style: TextStyle(fontSize: 10, color: Colors.grey[500])),
Text(label, style: TextStyle(fontSize: 13, color: Colors.grey[500])),
]),
),
),
@@ -311,7 +312,7 @@ class _FeatureChip extends StatelessWidget {
required this.onTap,
});
static const _c = Color(0xFF6C5CE7);
static const _c = AppTheme.primary;
@override
Widget build(BuildContext context) {
@@ -326,9 +327,9 @@ class _FeatureChip extends StatelessWidget {
borderRadius: BorderRadius.circular(12),
),
child: Column(mainAxisSize: MainAxisSize.min, children: [
Icon(icon, size: 18, color: _c),
Icon(icon, size: 21, color: _c),
const SizedBox(height: 6),
Text(label, style: const TextStyle(fontSize: 10, fontWeight: FontWeight.w500, color: Color(0xFF666666))),
Text(label, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: Color(0xFF666666))),
]),
),
),

View File

@@ -185,11 +185,11 @@ class ServicePackageCard extends ConsumerWidget {
color: Color(0xFFFFF8EE),
shape: BoxShape.circle,
),
child: Icon(item.icon, size: 20, color: const Color(0xFFF5A623)),
child: Icon(item.icon, size: 23, color: const Color(0xFFF5A623)),
),
const SizedBox(height: 4),
Text(item.label,
style: const TextStyle(fontSize: 11, color: AppTheme.textSub),
style: const TextStyle(fontSize: 14, color: AppTheme.textSub),
textAlign: TextAlign.center,
),
],
@@ -212,14 +212,14 @@ class ServicePackageCard extends ConsumerWidget {
children: [
// 标题行 + 详情入口
Row(children: [
Text(package.title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: AppTheme.text)),
Text(package.title, style: const TextStyle(fontSize: 19, fontWeight: FontWeight.w600, color: AppTheme.text)),
const Spacer(),
GestureDetector(
onTap: () => pushRoute(ref, 'servicePackageDetail', params: {'id': package.id}),
child: Row(mainAxisSize: MainAxisSize.min, children: [
Text('详情', style: TextStyle(fontSize: 13, color: AppTheme.textSub)),
Text('详情', style: TextStyle(fontSize: 16, color: AppTheme.textSub)),
const SizedBox(width: 2),
Icon(Icons.chevron_right, size: 18, color: AppTheme.textHint),
Icon(Icons.chevron_right, size: 21, color: AppTheme.textHint),
]),
),
]),
@@ -232,9 +232,9 @@ class ServicePackageCard extends ConsumerWidget {
borderRadius: BorderRadius.circular(6),
),
child: const Row(mainAxisSize: MainAxisSize.min, children: [
Text('VIP', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w700, color: Colors.white)),
Text('VIP', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w700, color: Colors.white)),
SizedBox(width: 4),
Text('VIP 产品权益', style: TextStyle(fontSize: 11, fontWeight: FontWeight.w500, color: Colors.white)),
Text('VIP 产品权益', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)),
]),
),
const SizedBox(height: 14),
@@ -246,8 +246,8 @@ class ServicePackageCard extends ConsumerWidget {
child: GestureDetector(
onTap: () => pushRoute(ref, 'servicePackageDetail', params: {'id': package.id}),
child: Row(mainAxisSize: MainAxisSize.min, children: [
Text('查看更多服务包', style: TextStyle(fontSize: 13, color: AppTheme.primary, fontWeight: FontWeight.w500)),
Icon(Icons.chevron_right, size: 16, color: AppTheme.primary),
Text('查看更多服务包', style: TextStyle(fontSize: 16, color: AppTheme.primary, fontWeight: FontWeight.w500)),
Icon(Icons.chevron_right, size: 19, color: AppTheme.primary),
]),
),
),