Initial project

This commit is contained in:
Cody
2026-03-06 14:56:17 +08:00
parent 977b627b15
commit bf9e099747
1180 changed files with 50973 additions and 0 deletions

View File

@@ -0,0 +1,91 @@
import 'package:flutter/material.dart';
import 'colors.dart';
import 'font.dart';
/// 主题组装 -- 将 AppColors / AppFont 组装为 ThemeData
///
/// 同时提供 Light / Dark 双主题,按钮形状/颜色/字体统一在此定义,
/// AppButton 只负责变体切换和 loading 逻辑,不硬编码颜色和字体。
///
/// ## 数据流位置
///
/// ```
/// AppColors + AppFont (L1 常量)
/// → ★ AppTheme ★ (L1 组装) ← 你在这里
/// → MaterialApp(theme: AppTheme.theme, darkTheme: AppTheme.darkTheme)
/// → Theme.of(context) → 所有 Widget 自动响应主题变化
/// ```
///
/// ## 使用
///
/// ```dart
/// // app/app.dart
/// MaterialApp(
/// theme: AppTheme.theme, // getter 名与 MaterialApp 参数名一一对应
/// darkTheme: AppTheme.darkTheme,
/// )
/// ```
class AppTheme {
AppTheme._();
/// 亮色主题 — 对应 MaterialApp `theme:` 参数
static ThemeData get theme => _build(Brightness.light);
/// 暗色主题 — 对应 MaterialApp `darkTheme:` 参数
static ThemeData get darkTheme => _build(Brightness.dark);
static ThemeData _build(Brightness brightness) {
final isDark = brightness == Brightness.dark;
final primary = isDark ? AppColors.primaryLight : AppColors.primary;
return ThemeData(
useMaterial3: true,
brightness: brightness,
colorScheme: ColorScheme(
brightness: brightness,
primary: primary,
onPrimary: AppColors.white,
secondary: primary,
onSecondary: AppColors.white,
error: AppColors.error,
onError: AppColors.white,
surface: isDark ? AppColors.gray800 : AppColors.white,
onSurface: isDark ? AppColors.white : AppColors.gray900,
),
scaffoldBackgroundColor: isDark ? AppColors.gray900 : AppColors.gray50,
// 字体
textTheme: AppFont.textTheme(brightness),
// ElevatedButton → AppButton.primary
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primary,
foregroundColor: AppColors.white,
disabledBackgroundColor: AppColors.gray400,
minimumSize: const Size.fromHeight(48),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
),
// OutlinedButton → AppButton.secondary
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
foregroundColor: primary,
side: BorderSide(color: primary),
minimumSize: const Size.fromHeight(48),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
),
// TextButton → AppButton.text
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: primary,
minimumSize: const Size.fromHeight(48),
),
),
);
}
}

View File

@@ -0,0 +1,40 @@
import 'package:flutter/material.dart';
/// 颜色体系 — 与 Figma 设计稿对应
///
/// L1 基础常量 -- 不含任何 Widget只输出颜色常量。
/// View 层不直接引用 AppColors通过 Theme.of(context) 访问语义色;
/// 有特殊硬编码需求(插图、固定品牌色)时可直接引用。
///
/// ## 数据流位置
///
/// ```
/// AppColors颜色常量← 你在这里
/// → AppTheme组装为 ThemeData
/// → MaterialApp注入
/// → Theme.of(context)View 层消费)
/// ```
class AppColors {
AppColors._();
// ── Brand Primary ──────────────────────────────────────────────────────────
static const primary = Color(0xFF2F80ED);
static const primaryDark = Color(0xFF1A6BD4);
static const primaryLight = Color(0xFF5BA3F5);
// ── Semantic ───────────────────────────────────────────────────────────────
static const success = Color(0xFF27AE60);
static const warning = Color(0xFFF2C94C);
static const error = Color(0xFFEB5757);
// ── Neutral Gray Scale ─────────────────────────────────────────────────────
static const white = Color(0xFFFFFFFF);
static const gray50 = Color(0xFFF8F9FA);
static const gray100 = Color(0xFFF1F3F4);
static const gray200 = Color(0xFFE8EAED);
static const gray400 = Color(0xFFBDC1C6);
static const gray600 = Color(0xFF80868B);
static const gray800 = Color(0xFF3C4043);
static const gray900 = Color(0xFF202124);
static const black = Color(0xFF000000);
}

View File

@@ -0,0 +1,89 @@
import 'package:flutter/material.dart';
import 'font.dart';
/// 主题样式快捷封装
///
/// `context.styles` 返回此对象build 方法里一行获取所有样式,
/// 之后直接用 `s.bodySmall`、`s.primary`,不再写 Theme.of(context)。
///
/// ```dart
/// final s = context.styles;
///
/// Text('标题', style: s.titleMedium)
/// Text('描述', style: s.bodySmall)
/// Icon(Icons.home, color: s.primary)
/// Text('改色', style: s.bodySmall?.copyWith(color: s.primary))
/// ```
class AppStyles {
AppStyles(BuildContext context)
: _t = Theme.of(context).textTheme,
_c = Theme.of(context).colorScheme;
final TextTheme _t;
final ColorScheme _c;
// ── 字体 ──────────────────────────────────────────────────────────────────
TextStyle? get displayLarge => _t.displayLarge;
TextStyle? get displayMedium => _t.displayMedium;
TextStyle? get displaySmall => _t.displaySmall;
TextStyle? get headlineLarge => _t.headlineLarge;
TextStyle? get headlineMedium => _t.headlineMedium;
TextStyle? get headlineSmall => _t.headlineSmall;
TextStyle? get titleLarge => _t.titleLarge;
TextStyle? get titleMedium => _t.titleMedium;
TextStyle? get titleSmall => _t.titleSmall;
TextStyle? get bodyLarge => _t.bodyLarge;
TextStyle? get bodyMedium => _t.bodyMedium;
TextStyle? get bodySmall => _t.bodySmall;
TextStyle? get labelLarge => _t.labelLarge;
TextStyle? get labelMedium => _t.labelMedium;
TextStyle? get labelSmall => _t.labelSmall;
// ── 颜色 + 亮暗 ───────────────────────────────────────────────────────────
Brightness get brightness => _c.brightness;
bool get isDark => _c.brightness == Brightness.dark;
Color get primary => _c.primary;
Color get onPrimary => _c.onPrimary;
Color get secondary => _c.secondary;
Color get onSecondary => _c.onSecondary;
Color get error => _c.error;
Color get onError => _c.onError;
Color get surface => _c.surface;
Color get onSurface => _c.onSurface;
Color get outline => _c.outline;
Color get outlineVariant => _c.outlineVariant;
// ── 预组合样式(字体 + 颜色,开箱即用)──────────────────────────────────────
//
// 与 AppButton 变体理念一致:按语义选用,无需手动拼 TextStyle 或 copyWith。
// 新增场景时在此扩展,保持全局一致。
/// 分组标题 — 列表 Section、设置分组等sectionLabel 字体 + primary 色)
TextStyle get sectionLabel => AppFont.sectionLabel.copyWith(color: primary);
/// 辅助文字 — 元数据、次要信息、时间戳等labelMedium + outline 色)
TextStyle? get labelMuted => labelMedium?.copyWith(color: outline);
/// 正文次要 — 描述、提示等bodySmall + outline 色)
TextStyle? get bodyMuted => bodySmall?.copyWith(color: outline);
/// 错误提示 — 表单错误、警告等bodySmall + error 色)
TextStyle? get bodyError => bodySmall?.copyWith(color: error);
}
/// BuildContext 主题入口
///
/// ```dart
/// final s = context.styles;
/// ```
extension AppThemeX on BuildContext {
AppStyles get styles => AppStyles(this);
}

View File

@@ -0,0 +1,215 @@
import 'package:flutter/material.dart';
/// 字体体系 -- 与 Figma 设计稿对应
///
/// L1 基础常量 — 不含颜色,只定义字号/字重/行高/字距。
/// View 层通过 [AppStyles]`context.styles`)消费,颜色由主题决定。
/// 特殊场景(固定样式、不跟主题)可直接引用 AppFont。
///
/// ## 数据流位置
///
/// ```
/// AppFont字体常量← 你在这里
/// → AppTheme组装为 TextTheme → ThemeData
/// → MaterialApp注入
/// → context.stylesView 层消费)
/// ```
///
/// ## 使用
///
/// ```dart
/// // 推荐:通过 context.styles 消费(自动响应亮暗主题)
/// final s = context.styles;
/// Text('标题', style: s.headlineMedium);
/// Text('分组', style: s.sectionLabel); // 预组合:字体 + 主题色
///
/// // 特殊场景:固定样式,不跟主题切换
/// Text('固定', style: AppFont.bodyMedium);
/// ```
class AppFont {
AppFont._();
// ── 字体族 ──────────────────────────────────────────────────────────────
/// 默认字体族(系统字体)
///
/// 接入自定义字体时只需修改此常量 + pubspec.yaml fonts 配置。
static const String? _fontFamily = null; // null = 系统默认字体
// ── Display -- 超大展示(启动页、空状态大标题)──────────────────────────
static const displayLarge = TextStyle(
fontFamily: _fontFamily,
fontSize: 57,
fontWeight: FontWeight.w400,
letterSpacing: -0.25,
height: 64 / 57,
);
static const displayMedium = TextStyle(
fontFamily: _fontFamily,
fontSize: 45,
fontWeight: FontWeight.w400,
height: 52 / 45,
);
static const displaySmall = TextStyle(
fontFamily: _fontFamily,
fontSize: 36,
fontWeight: FontWeight.w400,
height: 44 / 36,
);
// ── Headline -- 页面标题导航栏、Section 标题)────────────────────────
static const headlineLarge = TextStyle(
fontFamily: _fontFamily,
fontSize: 32,
fontWeight: FontWeight.w400,
height: 40 / 32,
);
static const headlineMedium = TextStyle(
fontFamily: _fontFamily,
fontSize: 28,
fontWeight: FontWeight.w400,
height: 36 / 28,
);
static const headlineSmall = TextStyle(
fontFamily: _fontFamily,
fontSize: 24,
fontWeight: FontWeight.w400,
height: 32 / 24,
);
// ── Title -- 卡片 / 列表标题(聊天列表名称、设置项标题)──────────────
static const titleLarge = TextStyle(
fontFamily: _fontFamily,
fontSize: 22,
fontWeight: FontWeight.w500,
height: 28 / 22,
);
static const titleMedium = TextStyle(
fontFamily: _fontFamily,
fontSize: 16,
fontWeight: FontWeight.w500,
letterSpacing: 0.15,
height: 24 / 16,
);
static const titleSmall = TextStyle(
fontFamily: _fontFamily,
fontSize: 14,
fontWeight: FontWeight.w500,
letterSpacing: 0.1,
height: 20 / 14,
);
// ── Body -- 正文内容(聊天气泡、表单输入、描述文字)──────────────────
static const bodyLarge = TextStyle(
fontFamily: _fontFamily,
fontSize: 16,
fontWeight: FontWeight.w400,
letterSpacing: 0.5,
height: 24 / 16,
);
static const bodyMedium = TextStyle(
fontFamily: _fontFamily,
fontSize: 14,
fontWeight: FontWeight.w400,
letterSpacing: 0.25,
height: 20 / 14,
);
static const bodySmall = TextStyle(
fontFamily: _fontFamily,
fontSize: 12,
fontWeight: FontWeight.w400,
letterSpacing: 0.4,
height: 16 / 12,
);
// ── Label -- 按钮 / 标签 / 辅助文字按钮文字、Tab、Badge──────────
static const labelLarge = TextStyle(
fontFamily: _fontFamily,
fontSize: 14,
fontWeight: FontWeight.w500,
letterSpacing: 0.1,
height: 20 / 14,
);
static const labelMedium = TextStyle(
fontFamily: _fontFamily,
fontSize: 12,
fontWeight: FontWeight.w500,
letterSpacing: 0.5,
height: 16 / 12,
);
static const labelSmall = TextStyle(
fontFamily: _fontFamily,
fontSize: 11,
fontWeight: FontWeight.w500,
letterSpacing: 0.5,
height: 16 / 11,
);
// ── 语义字体(超出 M3 标准级别的产品专属规格)────────────────────────────
//
// 这里只定义字号/字重/字距,不含颜色。
// 颜色由 AppStyles 的预组合样式注入(如 AppStyles.sectionLabel
/// 分组标题:列表 Section、设置分组等13 / w600 / 0.5 字距)
static const sectionLabel = TextStyle(
fontFamily: _fontFamily,
fontSize: 13,
fontWeight: FontWeight.w600,
letterSpacing: 0.5,
);
// ── 组装 TextTheme供 AppTheme 调用)──────────────────────────────────
/// 根据亮暗模式组装 TextTheme
///
/// 默认亮暗共用同一套字体规格。需要按模式区分时,
/// 用 copyWith 覆盖个别样式即可,不影响其他级别。
///
/// 示例 -- 暗色模式下 labelLarge 改为 regular
/// ```dart
/// labelLarge: isDark
/// ? labelLarge.copyWith(fontWeight: FontWeight.w400)
/// : labelLarge,
/// ```
///
/// AppTheme._build() 中调用:
/// ```dart
/// textTheme: AppFont.textTheme(brightness),
/// ```
static TextTheme textTheme(Brightness brightness) {
// final isDark = brightness == Brightness.dark;
return TextTheme(
displayLarge: displayLarge,
displayMedium: displayMedium,
displaySmall: displaySmall,
headlineLarge: headlineLarge,
headlineMedium: headlineMedium,
headlineSmall: headlineSmall,
titleLarge: titleLarge,
titleMedium: titleMedium,
titleSmall: titleSmall,
bodyLarge: bodyLarge,
bodyMedium: bodyMedium,
bodySmall: bodySmall,
labelLarge: labelLarge,
labelMedium: labelMedium,
labelSmall: labelSmall,
);
}
}

View File

@@ -0,0 +1,141 @@
import 'package:flutter/material.dart';
import '../base/context_theme_ext.dart';
/// # AppButton — 按钮原子组件L2 Component
///
/// 四种命名构造器对应四种变体loading 状态自动禁用点击。
/// 颜色和形状由 AppTheme 定义AppButton 只做变体切换和 loading 逻辑。
///
/// ## 数据流位置
///
/// ```
/// View 层 Widget 树
/// → ★ AppButton.primary / .secondary / .text / .inverse ★ ← 你在这里
/// → ElevatedButton / OutlinedButton / TextButton / FilledButton
/// → AppTheme颜色 / 形状已在 ThemeData 中定义)
/// ```
///
/// ## 使用
///
/// ```dart
/// // 主按钮(全宽,填充色)
/// AppButton.primary(label: '登录', onPressed: () => vm.login()),
///
/// // 加载状态(禁用点击,显示进度圈)
/// AppButton.primary(label: '登录', onPressed: null, isLoading: true),
///
/// // 副按钮(描边)
/// AppButton.secondary(label: '注册', onPressed: () {}),
///
/// // 文字按钮(非全宽)
/// AppButton.text(label: '忘记密码?', onPressed: () {}),
///
/// // 反色按钮:亮色模式黑底白字,暗色模式白底黑字
/// AppButton.inverse(
/// label: '切换 Tab',
/// icon: const Icon(Icons.swap_horiz),
/// onPressed: () {},
/// ),
/// ```
enum _ButtonVariant { primary, secondary, text, inverse }
class AppButton extends StatelessWidget {
const AppButton.primary({
super.key,
required this.label,
this.onPressed,
this.isLoading = false,
this.fullWidth = true,
}) : _variant = _ButtonVariant.primary,
icon = null;
const AppButton.secondary({
super.key,
required this.label,
this.onPressed,
this.isLoading = false,
this.fullWidth = true,
}) : _variant = _ButtonVariant.secondary,
icon = null;
const AppButton.text({
super.key,
required this.label,
this.onPressed,
this.isLoading = false,
this.fullWidth = false,
}) : _variant = _ButtonVariant.text,
icon = null;
/// 反色按钮:颜色随明暗主题取反。
///
/// 亮色模式:黑色背景 + 白色文字。
/// 暗色模式:白色背景 + 黑色文字。
///
/// 可选传 [icon]`Icon` widget自动切换为带图标布局。
const AppButton.inverse({
super.key,
required this.label,
this.onPressed,
this.isLoading = false,
this.fullWidth = false,
this.icon,
}) : _variant = _ButtonVariant.inverse;
final String label;
final VoidCallback? onPressed;
final bool isLoading;
final bool fullWidth;
/// 仅 [AppButton.inverse] 使用,其余变体固定为 null
final Widget? icon;
final _ButtonVariant _variant;
@override
Widget build(BuildContext context) {
final label = isLoading
? const SizedBox.square(
dimension: 20,
child: CircularProgressIndicator.adaptive(strokeWidth: 2),
)
: Text(this.label);
final button = switch (_variant) {
_ButtonVariant.primary =>
ElevatedButton(onPressed: isLoading ? null : onPressed, child: label),
_ButtonVariant.secondary =>
OutlinedButton(onPressed: isLoading ? null : onPressed, child: label),
_ButtonVariant.text =>
TextButton(onPressed: isLoading ? null : onPressed, child: label),
_ButtonVariant.inverse => _buildInverse(context, label),
};
return fullWidth ? SizedBox(width: double.infinity, child: button) : button;
}
Widget _buildInverse(BuildContext context, Widget label) {
final s = context.styles;
final isDark = s.isDark;
final bg = isDark ? Colors.white : Colors.black;
final fg = isDark ? Colors.black : Colors.white;
final style = FilledButton.styleFrom(
backgroundColor: bg,
foregroundColor: fg,
);
if (icon != null) {
return FilledButton.icon(
onPressed: isLoading ? null : onPressed,
style: style,
icon: icon!,
label: label,
);
}
return FilledButton(
onPressed: isLoading ? null : onPressed,
style: style,
child: label,
);
}
}

View File

@@ -0,0 +1,89 @@
import 'package:flutter/material.dart';
import '../components/app_button.dart';
/// # AppDialog — 业务确认弹窗L3 Composite
///
/// 封装 showDialog统一弹窗交互规范标题 + 内容 + 确认/取消)。
/// 内部使用 AppButton展示 L3 → L2 → L1 的完整组合链路。
///
/// ## 数据流位置
///
/// ```
/// View 层调用
/// → AppDialog.show() ← 你在这里(静态入口)
/// → showDialog<bool>
/// → AppDialog widgetAlertDialog 布局)
/// → AppButton.text取消
/// → AppButton.primary确认
/// ← Future<bool?> → true=确认, false=取消, null=点背景关闭
/// ```
///
/// ## 使用
///
/// ```dart
/// // View 层
/// final confirmed = await AppDialog.show(
/// context,
/// title: '删除联系人',
/// content: '确定要删除该联系人吗?此操作不可恢复。',
/// confirmLabel: '删除',
/// );
/// if (confirmed == true) {
/// ref.read(contactViewModelProvider.notifier).deleteContact(id);
/// }
/// ```
class AppDialog extends StatelessWidget {
const AppDialog._({
required this.title,
required this.content,
required this.confirmLabel,
this.cancelLabel,
});
final String title;
final String content;
final String confirmLabel;
final String? cancelLabel;
/// 显示确认弹窗
///
/// 返回:`true` = 确认,`false` = 取消,`null` = 点背景关闭
static Future<bool?> show(
BuildContext context, {
required String title,
required String content,
String confirmLabel = '确定', // TODO: 接入国际化
String? cancelLabel = '取消', // TODO: 接入国际化
bool barrierDismissible = true,
}) =>
showDialog<bool>(
context: context,
barrierDismissible: barrierDismissible,
builder: (_) => AppDialog._(
title: title,
content: content,
confirmLabel: confirmLabel,
cancelLabel: cancelLabel,
),
);
@override
Widget build(BuildContext context) => AlertDialog(
title: Text(title),
content: Text(content),
actions: [
if (cancelLabel != null)
AppButton.text(
label: cancelLabel!,
fullWidth: false,
onPressed: () => Navigator.of(context).pop(false),
),
AppButton.primary(
label: confirmLabel,
fullWidth: false,
onPressed: () => Navigator.of(context).pop(true),
),
],
);
}