8d890c0424
- 新增 lib/core/network/:ApiClient(dio 封装,错误信封→类型化异常,base URL 经 --dart-define=PATBOND_API_BASE_URL 注入)、AuthInterceptor(Bearer + 设备标识)、TokenRefresher(单飞刷新,401/40101 重放一次,仅 40102/再 401 清会话) - 新增 lib/features/auth/:SessionManager(token 只进 flutter_secure_storage)、AuthRepository(login/register/refresh/logout/me,注册带 Idempotency-Key)、Splash(500ms 最短停留/5s 超时/错误态重试+改用账号登录)、登录页与注册页(照 12 号组装稿,错误三层映射,预留区不渲染) - App 根组件改为认证状态机驱动(Splash↔登录↔主壳 300ms fade);个人中心退出登录接入真实 logout - 顺带修复:FIX-1 促销卡渐变改 [primaryStrong, primary]、FIX-2 补 helperStyle: muted、README dart format 命令补 --output=none - 新增依赖:dio ^5.11.1、flutter_secure_storage ^11.0.0、uuid ^4.6.0 - 门禁:dart format(0 changed)/ flutter analyze(No issues)/ flutter test(30 passed,其中新增 23:TokenRefresher 5 + AuthRepository 10 + 登录页 4 + 注册页 4) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
189 lines
6.2 KiB
Dart
189 lines
6.2 KiB
Dart
import 'package:flutter/material.dart';
|
||
|
||
/// 语义色 token(ADR-005:以 AI宠物_iOS_UI设计稿.html 珊瑚橙暖色系为品牌正典,
|
||
/// 详见 iteration-1-reports/04-ui-login-design-spec.md §1.1)。
|
||
abstract final class AppColors {
|
||
/// 品牌珊瑚橙:图标、装饰、渐变起点;不用于承载文字的实心填充(对比度不足)。
|
||
static const primary = Color(0xFFFF6F4C);
|
||
|
||
/// 加深珊瑚:实心按钮填充、可点击文字链接(白字约 4.5:1,WCAG AA)。
|
||
static const primaryStrong = Color(0xFFD6431A);
|
||
|
||
/// 浅色底上的强调文字、按钮反白替代。
|
||
static const primaryDark = Color(0xFF7A2E12);
|
||
|
||
/// amber:渐变终点、徽章、会员/促销 CTA。
|
||
static const accent = Color(0xFFFFB648);
|
||
|
||
/// amber 底上的文字。
|
||
static const accentDark = Color(0xFF7A4B0A);
|
||
|
||
/// 页面背景(奶油底)。
|
||
static const canvas = Color(0xFFFFF7ED);
|
||
|
||
/// 卡片、输入框背景。
|
||
static const surface = Color(0xFFFFFFFF);
|
||
|
||
/// peach:图标底、占位块、选中指示。
|
||
static const surfaceTint = Color(0xFFFFE8D6);
|
||
|
||
/// 主文字。
|
||
static const ink = Color(0xFF3E2A1F);
|
||
|
||
/// 次级文字、占位符。
|
||
static const muted = Color(0xFF9C8977);
|
||
|
||
/// 描边、分隔线。
|
||
static const border = Color(0xFFF0DCC8);
|
||
|
||
/// sage:成功提示的图标/描边。
|
||
static const success = Color(0xFF7FA88A);
|
||
|
||
/// 成功提示底上的文字。
|
||
static const successInk = Color(0xFF3F5744);
|
||
|
||
/// 成功提示底色。
|
||
static const successSurface = Color(0xFFE8F0E8);
|
||
|
||
static const warning = Color(0xFFF59E0B);
|
||
|
||
/// 校验错误文字、错误描边、失败提示(白底上约 5.4:1,AA 达标)。
|
||
static const error = Color(0xFFD0342C);
|
||
|
||
/// 品牌渐变:hero 区、头像环、装饰性大面积(不承载正文文字)。
|
||
static const brandGradient = LinearGradient(
|
||
begin: Alignment.topLeft,
|
||
end: Alignment.bottomRight,
|
||
colors: [primary, accent],
|
||
);
|
||
}
|
||
|
||
/// 圆角 token(设计规范 §1.3)。
|
||
abstract final class AppRadius {
|
||
/// 小按钮、内嵌 CTA、错误横幅。
|
||
static const sm = 12.0;
|
||
|
||
/// 主按钮、SnackBar、菜单组。
|
||
static const md = 16.0;
|
||
|
||
/// 输入框、feed 卡。
|
||
static const lg = 18.0;
|
||
|
||
/// 大卡片。
|
||
static const xl = 24.0;
|
||
|
||
/// 芯片、徽章。
|
||
static const pill = 999.0;
|
||
}
|
||
|
||
ThemeData buildAppTheme() {
|
||
final scheme = ColorScheme.fromSeed(
|
||
seedColor: AppColors.primary,
|
||
brightness: Brightness.light,
|
||
surface: AppColors.surface,
|
||
).copyWith(error: AppColors.error, onError: Colors.white);
|
||
|
||
return ThemeData(
|
||
useMaterial3: true,
|
||
colorScheme: scheme,
|
||
scaffoldBackgroundColor: AppColors.canvas,
|
||
fontFamilyFallback: const [
|
||
'Noto Sans CJK SC',
|
||
'Noto Sans SC',
|
||
'PingFang SC',
|
||
'Microsoft YaHei',
|
||
'sans-serif',
|
||
],
|
||
textTheme: const TextTheme(
|
||
headlineSmall: TextStyle(
|
||
color: AppColors.ink,
|
||
fontSize: 22,
|
||
fontWeight: FontWeight.w800,
|
||
),
|
||
titleLarge: TextStyle(
|
||
color: AppColors.ink,
|
||
fontSize: 18,
|
||
fontWeight: FontWeight.w800,
|
||
),
|
||
titleMedium: TextStyle(
|
||
color: AppColors.ink,
|
||
fontSize: 15,
|
||
fontWeight: FontWeight.w700,
|
||
),
|
||
bodyMedium: TextStyle(color: AppColors.ink, fontSize: 14, height: 1.5),
|
||
bodySmall: TextStyle(color: AppColors.muted, fontSize: 12, height: 1.4),
|
||
),
|
||
cardTheme: const CardThemeData(
|
||
color: AppColors.surface,
|
||
elevation: 0,
|
||
margin: EdgeInsets.zero,
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.all(Radius.circular(AppRadius.xl)),
|
||
side: BorderSide(color: AppColors.border),
|
||
),
|
||
),
|
||
filledButtonTheme: FilledButtonThemeData(
|
||
style: FilledButton.styleFrom(
|
||
backgroundColor: AppColors.primaryStrong,
|
||
foregroundColor: Colors.white,
|
||
disabledBackgroundColor: AppColors.ink.withAlpha(31),
|
||
disabledForegroundColor: AppColors.ink.withAlpha(97),
|
||
minimumSize: const Size(64, 52),
|
||
shape: const RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.all(Radius.circular(AppRadius.md)),
|
||
),
|
||
textStyle: const TextStyle(fontSize: 15, fontWeight: FontWeight.w700),
|
||
),
|
||
),
|
||
inputDecorationTheme: const InputDecorationTheme(
|
||
filled: true,
|
||
fillColor: AppColors.surface,
|
||
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||
border: OutlineInputBorder(
|
||
borderRadius: BorderRadius.all(Radius.circular(AppRadius.lg)),
|
||
borderSide: BorderSide(color: AppColors.border),
|
||
),
|
||
enabledBorder: OutlineInputBorder(
|
||
borderRadius: BorderRadius.all(Radius.circular(AppRadius.lg)),
|
||
borderSide: BorderSide(color: AppColors.border),
|
||
),
|
||
focusedBorder: OutlineInputBorder(
|
||
borderRadius: BorderRadius.all(Radius.circular(AppRadius.lg)),
|
||
borderSide: BorderSide(color: AppColors.primary, width: 1.5),
|
||
),
|
||
errorBorder: OutlineInputBorder(
|
||
borderRadius: BorderRadius.all(Radius.circular(AppRadius.lg)),
|
||
borderSide: BorderSide(color: AppColors.error, width: 1.5),
|
||
),
|
||
focusedErrorBorder: OutlineInputBorder(
|
||
borderRadius: BorderRadius.all(Radius.circular(AppRadius.lg)),
|
||
borderSide: BorderSide(color: AppColors.error, width: 1.5),
|
||
),
|
||
errorStyle: TextStyle(color: AppColors.error, fontSize: 12),
|
||
helperStyle: TextStyle(color: AppColors.muted, fontSize: 12),
|
||
),
|
||
snackBarTheme: const SnackBarThemeData(
|
||
behavior: SnackBarBehavior.floating,
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.all(Radius.circular(AppRadius.md)),
|
||
),
|
||
),
|
||
navigationBarTheme: NavigationBarThemeData(
|
||
backgroundColor: AppColors.surface,
|
||
indicatorColor: AppColors.surfaceTint,
|
||
height: 72,
|
||
labelTextStyle: WidgetStateProperty.resolveWith((states) {
|
||
return TextStyle(
|
||
fontSize: 11,
|
||
fontWeight: states.contains(WidgetState.selected)
|
||
? FontWeight.w700
|
||
: FontWeight.w500,
|
||
color: states.contains(WidgetState.selected)
|
||
? AppColors.primaryStrong
|
||
: AppColors.muted,
|
||
);
|
||
}),
|
||
),
|
||
);
|
||
}
|