chore: 初始化脚手架 — sunny_mochi 企业级 Flutter 模板

Core 基础设施:Flavor 三包体系、Drift+SQLCipher 加密数据库
7 拦截器 Dio 网络栈、CrashReporter 崩溃日志、Sealed Failure 错误体系
5 色板主题系统、Auth 认证骨架(Clean Architecture)、Dev Panel、Mock Adapter

通过验证:flutter analyze(0 errors)、flutter test(全绿)、staging APK 74.8MB
This commit is contained in:
SkyJourney
2026-05-14 12:51:05 +08:00
commit 61017f1c39
204 changed files with 15386 additions and 0 deletions
+200
View File
@@ -0,0 +1,200 @@
import 'package:flutter/material.dart';
/// 5 套主题色板(white 为 Flutter 新增默认主题,其余 4 套与 iOS plist 1:1 对齐)。
///
/// Flutter 默认 `white`(白色导航栏 + 中性灰背景 + 紫色点缀,对应设计稿默认配色)。
/// iOS 默认 `purple`AppThemeManager.swift:44 `?? "purple"`)— 两端可独立配置。
enum AppColorScheme { white, blue, red, green, purple }
/// 单套主题色板 — 对应一个 plist 文件的 10 个色值 key。
///
/// 命名与 iOS ThemeKey 1:1 对应;iOS 用 `view.theme_backgroundColor = ThemeKey.backgroundColor` 绑定,
/// Flutter 通过 [Theme.of(context).extension<AppPalette>()] 读取。
@immutable
class AppPalette extends ThemeExtension<AppPalette> {
const AppPalette({
required this.primary,
required this.secondary,
required this.background,
required this.text,
required this.navBar,
required this.navBarText,
required this.buttonBg,
required this.buttonText,
required this.tabBarSelected,
required this.tabBarNormal,
});
final Color primary;
final Color secondary;
final Color background;
final Color text;
final Color navBar;
final Color navBarText;
final Color buttonBg;
final Color buttonText;
final Color tabBarSelected;
final Color tabBarNormal;
@override
AppPalette copyWith({
Color? primary,
Color? secondary,
Color? background,
Color? text,
Color? navBar,
Color? navBarText,
Color? buttonBg,
Color? buttonText,
Color? tabBarSelected,
Color? tabBarNormal,
}) {
return AppPalette(
primary: primary ?? this.primary,
secondary: secondary ?? this.secondary,
background: background ?? this.background,
text: text ?? this.text,
navBar: navBar ?? this.navBar,
navBarText: navBarText ?? this.navBarText,
buttonBg: buttonBg ?? this.buttonBg,
buttonText: buttonText ?? this.buttonText,
tabBarSelected: tabBarSelected ?? this.tabBarSelected,
tabBarNormal: tabBarNormal ?? this.tabBarNormal,
);
}
@override
AppPalette lerp(ThemeExtension<AppPalette>? other, double t) {
if (other is! AppPalette) return this;
return AppPalette(
primary: Color.lerp(primary, other.primary, t)!,
secondary: Color.lerp(secondary, other.secondary, t)!,
background: Color.lerp(background, other.background, t)!,
text: Color.lerp(text, other.text, t)!,
navBar: Color.lerp(navBar, other.navBar, t)!,
navBarText: Color.lerp(navBarText, other.navBarText, t)!,
buttonBg: Color.lerp(buttonBg, other.buttonBg, t)!,
buttonText: Color.lerp(buttonText, other.buttonText, t)!,
tabBarSelected: Color.lerp(tabBarSelected, other.tabBarSelected, t)!,
tabBarNormal: Color.lerp(tabBarNormal, other.tabBarNormal, t)!,
);
}
}
/// 5 套色板 — white 为 Flutter 默认主题,其余 4 套与 iOS plist 完全一致。
abstract class AppPalettes {
/// 白色默认主题:白色导航栏 + 深色文字 + 中性灰背景 + 紫色点缀。
/// 对应设计稿默认白灰配色(无色调染色)。
///
/// 字色 #252535 与设计稿其他地方一致(手机号/列表项/标题统一字色)。
static const AppPalette white = AppPalette(
primary: Color(0xFF947DFF),
secondary: Color(0xFFA893FF),
background: Color(0xFFF5F6F8),
text: Color(0xFF252535),
navBar: Color(0xFFFFFFFF),
navBarText: Color(0xFF252535),
buttonBg: Color(0xFF947DFF),
buttonText: Color(0xFFFFFFFF),
tabBarSelected: Color(0xFF947DFF),
tabBarNormal: Color(0xFF77849E),
);
static const AppPalette blue = AppPalette(
primary: Color(0xFF3366FF),
secondary: Color(0xFF4D7FFF),
background: Color(0xFFF0F4FF),
text: Color(0xFF333333),
navBar: Color(0xFF3366FF),
navBarText: Color(0xFFFFFFFF),
buttonBg: Color(0xFF3366FF),
buttonText: Color(0xFFFFFFFF),
tabBarSelected: Color(0xFF3366FF),
tabBarNormal: Color(0xFF999999),
);
static const AppPalette red = AppPalette(
primary: Color(0xFFE63333),
secondary: Color(0xFFFF4D4D),
background: Color(0xFFFFF5F5),
text: Color(0xFF333333),
navBar: Color(0xFFE63333),
navBarText: Color(0xFFFFFFFF),
buttonBg: Color(0xFFE63333),
buttonText: Color(0xFFFFFFFF),
tabBarSelected: Color(0xFFE63333),
tabBarNormal: Color(0xFF999999),
);
static const AppPalette green = AppPalette(
primary: Color(0xFF33A855),
secondary: Color(0xFF4DC46A),
background: Color(0xFFF0FFF4),
text: Color(0xFF333333),
navBar: Color(0xFF33A855),
navBarText: Color(0xFFFFFFFF),
buttonBg: Color(0xFF33A855),
buttonText: Color(0xFFFFFFFF),
tabBarSelected: Color(0xFF33A855),
tabBarNormal: Color(0xFF999999),
);
static const AppPalette purple = AppPalette(
primary: Color(0xFF947DFF),
secondary: Color(0xFF947DFF),
background: Color(0xFFF8F0FF),
text: Color(0xFF333333),
navBar: Color(0xFF947DFF),
navBarText: Color(0xFFFFFFFF),
buttonBg: Color(0xFF947DFF),
buttonText: Color(0xFFFFFFFF),
tabBarSelected: Color(0xFF947DFF),
tabBarNormal: Color(0xFF999999),
);
static const Map<AppColorScheme, AppPalette> all = {
AppColorScheme.white: white,
AppColorScheme.blue: blue,
AppColorScheme.red: red,
AppColorScheme.green: green,
AppColorScheme.purple: purple,
};
static AppPalette of(AppColorScheme scheme) => all[scheme]!;
/// 中文展示名(设置页主题切换 UI 用)
static String displayName(AppColorScheme scheme) => switch (scheme) {
AppColorScheme.white => '默认',
AppColorScheme.blue => '蓝色',
AppColorScheme.red => '红色',
AppColorScheme.green => '绿色',
AppColorScheme.purple => '紫色',
};
}
/// 通用色(与主题无关,跨 4 套主题不变) — 文字 / 背景 / 状态色。
abstract class AppColors {
// 4 套主题色映射(向后兼容旧调用,新代码请用 AppPalettes.of
static Map<AppColorScheme, Color> get primary => {
for (final e in AppPalettes.all.entries) e.key: e.value.primary,
};
// 通用色彩(4 套主题共用)
static const Color surface = Color(0xFFFFFFFF);
static const Color divider = Color(0xFFEEEEEE);
static const Color textPrimary = Color(0xFF333333);
static const Color textSecondary = Color(0xFF666666);
static const Color textHint = Color(0xFF999999);
static const Color textPlaceholder = Color(0xFFBBBBBB);
static const Color error = Color(0xFFFF4D4F);
static const Color warning = Color(0xFFFA8C16);
static const Color success = Color(0xFF52C41A);
// 兼容旧引用(这些常量已退化为 fallback / 跨主题中性色)
static const Color background = Color(0xFFF5F5F5);
static const Color navBar = Color(0xFFFFFFFF);
static const Color navBarDark = Color(0xFF1A1A1A);
static const Color tabBarUnselected = Color(0xFF999999);
}
+119
View File
@@ -0,0 +1,119 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:sunny_mochi/core/theme/app_colors.dart';
/// 应用主题工厂 — 按 [AppColorScheme] 真实切换全部色(对齐 iOS 4 套 plist)。
///
/// 与旧版差异:
/// - 旧版只把 primary 当作 seednav/btn/bg 都写死,导致 4 套主题视觉差异不明显
/// - 新版从 [AppPalettes] 读完整 10 色,AppBar/ColorScheme/extensions 全部按主题切
/// - 通过 ThemeExtension 暴露 [AppPalette],业务代码 `Theme.of(context).extension<AppPalette>()!.buttonBg` 读
abstract class AppTheme {
static ThemeData light({AppColorScheme scheme = AppColorScheme.white}) {
final palette = AppPalettes.of(scheme);
return ThemeData(
useMaterial3: true,
// BackButton 图标:iOS 平台渲染 arrow_back_ios_new< 样式),与登录页一致。
// ScrollBehavior 使用 defaultTargetPlatform,不受此字段影响。
platform: TargetPlatform.iOS,
// 全局字体 — HarmonyOS Sans SCGB2312 子集,pubspec 注册)
fontFamily: 'HarmonyOS Sans SC',
colorScheme: ColorScheme.fromSeed(
seedColor: palette.primary,
primary: palette.primary,
secondary: palette.secondary,
surface: AppColors.surface,
error: AppColors.error,
),
scaffoldBackgroundColor: palette.background,
primaryColor: palette.primary,
appBarTheme: AppBarTheme(
backgroundColor: palette.navBar,
foregroundColor: palette.navBarText,
elevation: 0,
// Material 3 默认会在滚动时给 AppBar 叠加 surfaceTint 染色(紫色);
// 设计稿要求 navBar 保持纯白,必须显式置为 transparent + 0 elevation。
surfaceTintColor: Colors.transparent,
scrolledUnderElevation: 0,
centerTitle: true,
titleTextStyle: TextStyle(
color: palette.navBarText,
fontSize: 18.sp,
fontWeight: FontWeight.w600,
),
iconTheme: IconThemeData(color: palette.navBarText, size: 22.sp),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: palette.buttonBg,
foregroundColor: palette.buttonText,
),
),
filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom(
backgroundColor: palette.buttonBg,
foregroundColor: palette.buttonText,
),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(foregroundColor: palette.primary),
),
tabBarTheme: TabBarThemeData(
labelColor: palette.tabBarSelected,
unselectedLabelColor: palette.tabBarNormal,
indicatorColor: palette.tabBarSelected,
),
bottomNavigationBarTheme: BottomNavigationBarThemeData(
selectedItemColor: palette.tabBarSelected,
unselectedItemColor: palette.tabBarNormal,
),
textTheme: TextTheme(
bodyLarge: TextStyle(fontSize: 16.sp, color: palette.text),
bodyMedium: TextStyle(fontSize: 14.sp, color: AppColors.textSecondary),
bodySmall: TextStyle(fontSize: 12.sp, color: AppColors.textHint),
titleLarge: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w600,
color: palette.text,
),
),
dividerColor: AppColors.divider,
dividerTheme: const DividerThemeData(space: 1, thickness: 1),
extensions: [palette],
);
}
static ThemeData dark({AppColorScheme scheme = AppColorScheme.white}) {
final palette = AppPalettes.of(scheme);
return ThemeData(
useMaterial3: true,
platform: TargetPlatform.iOS,
brightness: Brightness.dark,
colorScheme: ColorScheme.fromSeed(
seedColor: palette.primary,
primary: palette.primary,
secondary: palette.secondary,
brightness: Brightness.dark,
),
primaryColor: palette.primary,
scaffoldBackgroundColor: const Color(0xFF121212),
appBarTheme: AppBarTheme(
backgroundColor: AppColors.navBarDark,
foregroundColor: Colors.white,
elevation: 0,
centerTitle: true,
titleTextStyle: TextStyle(
color: Colors.white,
fontSize: 17.sp,
fontWeight: FontWeight.w600,
),
),
bottomNavigationBarTheme: BottomNavigationBarThemeData(
selectedItemColor: palette.tabBarSelected,
unselectedItemColor: palette.tabBarNormal,
),
extensions: [palette],
);
}
}
+32
View File
@@ -0,0 +1,32 @@
import 'package:sunny_mochi/core/theme/app_colors.dart';
/// 主题化图片资源分发工具。
///
/// 约定:`assets/themes/{scheme}/{name}.png`,每套主题放同名文件。
/// 调用:`Image.asset(ThemeAssets.tabHomeNormal(scheme))`
///
/// TODO: 业务项目按需扩展此类,添加与品牌设计对应的主题图片路径。
/// 若 TabBar 使用 Material Icons(当前脚手架默认),此类可不使用。
abstract class ThemeAssets {
static const String _root = 'assets/themes';
static String _dir(AppColorScheme scheme) => '$_root/${scheme.name}';
// ═══ Tab 图标(脚手架默认 2 Tab,业务项目按需扩展)══════════════════════
/// Home Tab
static String tabHomeNormal(AppColorScheme scheme) =>
'${_dir(scheme)}/tab_home_normal.png';
static String tabHomeSelected(AppColorScheme scheme) =>
'${_dir(scheme)}/tab_home_selected.png';
/// Mine Tab
static String tabMineNormal(AppColorScheme scheme) =>
'${_dir(scheme)}/tab_mine_normal.png';
static String tabMineSelected(AppColorScheme scheme) =>
'${_dir(scheme)}/tab_mine_selected.png';
// ═══ 业务图片占位(TODO: 填入项目实际图片路径)════════════════════════════
// static String heroBanner(AppColorScheme scheme) =>
// '${_dir(scheme)}/hero_banner.png';
}
+76
View File
@@ -0,0 +1,76 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:sunny_mochi/core/theme/app_colors.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shared_preferences/shared_preferences.dart';
part 'theme_notifier.g.dart';
/// 当前色板 Notifier — 对应 iOS AppThemeManager。
///
/// **默认 white**(白色导航栏 + 中性灰背景,对应设计稿默认配色)。
/// 切换持久化到 SharedPreferences,下次启动恢复。
@Riverpod(keepAlive: true)
class ThemeNotifier extends _$ThemeNotifier {
static const _keyScheme = 'color_scheme';
@override
AppColorScheme build() {
unawaited(_restore());
return AppColorScheme.white;
}
/// 切换主题色板(同时持久化)
void switchScheme(AppColorScheme scheme) {
state = scheme;
unawaited(
SharedPreferences.getInstance().then(
(prefs) => prefs.setString(_keyScheme, scheme.name),
),
);
}
Future<void> _restore() async {
final prefs = await SharedPreferences.getInstance();
final name = prefs.getString(_keyScheme);
if (name != null) {
state = AppColorScheme.values.firstWhere(
(e) => e.name == name,
orElse: () => AppColorScheme.white,
);
}
}
}
/// 明亮 / 暗色 / 跟随系统 模式(与 iOS UIInterfaceStyle 对齐)
@Riverpod(keepAlive: true)
class ThemeModeNotifier extends _$ThemeModeNotifier {
static const _keyMode = 'theme_mode';
@override
ThemeMode build() {
unawaited(_restore());
return ThemeMode.light;
}
void switchMode(ThemeMode mode) {
state = mode;
unawaited(
SharedPreferences.getInstance().then(
(prefs) => prefs.setString(_keyMode, mode.name),
),
);
}
Future<void> _restore() async {
final prefs = await SharedPreferences.getInstance();
final name = prefs.getString(_keyMode);
if (name != null) {
state = ThemeMode.values.firstWhere(
(e) => e.name == name,
orElse: () => ThemeMode.light,
);
}
}
}
+137
View File
@@ -0,0 +1,137 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'theme_notifier.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
/// 当前色板 Notifier — 对应 iOS AppThemeManager。
///
/// **默认 white**(白色导航栏 + 中性灰背景,对应设计稿默认配色)。
/// 切换持久化到 SharedPreferences,下次启动恢复。
@ProviderFor(ThemeNotifier)
final themeProvider = ThemeNotifierProvider._();
/// 当前色板 Notifier — 对应 iOS AppThemeManager。
///
/// **默认 white**(白色导航栏 + 中性灰背景,对应设计稿默认配色)。
/// 切换持久化到 SharedPreferences,下次启动恢复。
final class ThemeNotifierProvider
extends $NotifierProvider<ThemeNotifier, AppColorScheme> {
/// 当前色板 Notifier — 对应 iOS AppThemeManager。
///
/// **默认 white**(白色导航栏 + 中性灰背景,对应设计稿默认配色)。
/// 切换持久化到 SharedPreferences,下次启动恢复。
ThemeNotifierProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'themeProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$themeNotifierHash();
@$internal
@override
ThemeNotifier create() => ThemeNotifier();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(AppColorScheme value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<AppColorScheme>(value),
);
}
}
String _$themeNotifierHash() => r'e4ed9671d872b5f592f2e5c732f355cf2efa217a';
/// 当前色板 Notifier — 对应 iOS AppThemeManager。
///
/// **默认 white**(白色导航栏 + 中性灰背景,对应设计稿默认配色)。
/// 切换持久化到 SharedPreferences,下次启动恢复。
abstract class _$ThemeNotifier extends $Notifier<AppColorScheme> {
AppColorScheme build();
@$mustCallSuper
@override
void runBuild() {
final ref = this.ref as $Ref<AppColorScheme, AppColorScheme>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<AppColorScheme, AppColorScheme>,
AppColorScheme,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}
/// 明亮 / 暗色 / 跟随系统 模式(与 iOS UIInterfaceStyle 对齐)
@ProviderFor(ThemeModeNotifier)
final themeModeProvider = ThemeModeNotifierProvider._();
/// 明亮 / 暗色 / 跟随系统 模式(与 iOS UIInterfaceStyle 对齐)
final class ThemeModeNotifierProvider
extends $NotifierProvider<ThemeModeNotifier, ThemeMode> {
/// 明亮 / 暗色 / 跟随系统 模式(与 iOS UIInterfaceStyle 对齐)
ThemeModeNotifierProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'themeModeProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$themeModeNotifierHash();
@$internal
@override
ThemeModeNotifier create() => ThemeModeNotifier();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(ThemeMode value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<ThemeMode>(value),
);
}
}
String _$themeModeNotifierHash() => r'daa7db2830c3897ea7d834261955b00e35431e4c';
/// 明亮 / 暗色 / 跟随系统 模式(与 iOS UIInterfaceStyle 对齐)
abstract class _$ThemeModeNotifier extends $Notifier<ThemeMode> {
ThemeMode build();
@$mustCallSuper
@override
void runBuild() {
final ref = this.ref as $Ref<ThemeMode, ThemeMode>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<ThemeMode, ThemeMode>,
ThemeMode,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}