Files
photo-live/apps/mobile/lib/app/theme.dart
T
浅念 6074fc8ed3 feat(ui06): App line A0-A7 — nav pill removal, light segments, bare summary, token alignment to app-preview
- tokens:次文字 #69778E、soft/card-border/seg 色组、输入圆角 10(kRadiusField)
- theme:导航 indicatorColor 透明+显式选中蓝;卡片去阴影改 1px #EDF0F5 边框;输入边框同步
- 新组件 SegmentTabs(#EDF1F6 轨道/选中实蓝/light 浅蓝变体/null 无高亮)替换 M3 SegmentedButton
- 上传中心:摘要去白卡直排+主数字 32/40;completed 次级筛选无高亮语义保留
- 相册卡 scrim 30% 起 #0C1728E8;导航角标红底白字白边;登录主标题 27/700
- theme_test 三处旧规格断言随 UI06 值更新;397/397 全绿、analyze 0 issues
- 真机证据:MuMu + 测试服务器 API,登录/相册/详情/上传 4 页截图确认达标
- 登记偏差:语义色保持对比度合规深色版;视口矩阵随 G2;详见验收记录 §6

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-23 08:09:20 +08:00

270 lines
10 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/// UI-01 主题(视觉规范 §3.1 精确落地)。
///
/// 浅色基准:primary #1A73E8 / surface #FFFFFF / 页面背景 #F6F8FB /
/// 主文字 #172033 / 次文字 #5E6878 / 分隔线 #E5EAF1;等待色 #996300 承载
/// ColorScheme.secondary、错误 #B42318 承载 error。成功色**不进**
/// ColorScheme(Material 无该语义角色),以独立令牌提供(见 ui/tokens.dart,
/// 成功/等待/错误均另有深色提亮变体)。
///
/// 深色:语义对应色(深表面 + 浅文字 + 提亮的主色/等待色/错误色);图片
/// 与内容不做反色处理,沿用跟随系统的深浅色切换。容器色(secondary/
/// primaryContainer 等)由种子色派生,与 #1A73E8 同系。
///
/// 字阶(fontSize 走 TextScaler,随系统字体缩放;UI-03 §3 校准):24
/// 页面标题 / 17 分组标题 / 16-14 正文 / 13-12 辅助。组件:主按钮高 48
/// (区间 48-52)、输入框与按钮圆角 12、卡片圆角 16、Dialog 圆角 16、
/// BottomSheet 顶部圆角 16。动效时长常量见 ui/tokens.dart。
library;
import 'package:flutter/material.dart';
import '../ui/tokens.dart';
abstract final class AppTheme {
/// Material 种子色(容器色派生基准)。
static const Color seedColor = Color(0xFF1A73E8);
// ── 浅色基准值(方案 §3.1)──────────────────────────────────────────
static const Color _primary = Color(0xFF1A73E8);
static const Color _onPrimary = Color(0xFFFFFFFF);
static const Color _surface = Color(0xFFFFFFFF);
static const Color _onSurface = Color(0xFF172033);
static const Color _background = Color(0xFFF6F8FB);
static const Color _textSecondary = Color(0xFF69778E); // UI06: app-preview --muted
static const Color _divider = Color(0xFFE5EAF1);
static const Color _waiting = Color(0xFF996300);
static const Color _onWaiting = Color(0xFFFFFFFF);
static const Color _error = Color(0xFFB42318);
// ── 深色对应语义值(同语义、深色表面提亮)──────────────────────────
static const Color _primaryDark = Color(0xFF8AB4F8);
static const Color _onPrimaryDark = Color(0xFF0B2E6F);
static const Color _surfaceDark = Color(0xFF1F2530);
static const Color _onSurfaceDark = Color(0xFFE6EAF2);
static const Color _backgroundDark = Color(0xFF12161D);
static const Color _textSecondaryDark = Color(0xFFA6B0C0);
static const Color _dividerDark = Color(0xFF333B47);
static const Color _waitingDark = Color(0xFFFCD34D);
static const Color _onWaitingDark = Color(0xFF402E00);
static const Color _errorDark = Color(0xFFF87171);
static const Color _onErrorDark = Color(0xFF5C1A15);
static ThemeData get light => _build(Brightness.light);
static ThemeData get dark => _build(Brightness.dark);
static ThemeData _build(Brightness brightness) {
final dark = brightness == Brightness.dark;
final colorScheme =
ColorScheme.fromSeed(
seedColor: seedColor,
brightness: brightness,
).copyWith(
primary: dark ? _primaryDark : _primary,
onPrimary: dark ? _onPrimaryDark : _onPrimary,
secondary: dark ? _waitingDark : _waiting,
onSecondary: dark ? _onWaitingDark : _onWaiting,
error: dark ? _errorDark : _error,
onError: dark ? _onErrorDark : Colors.white,
surface: dark ? _surfaceDark : _surface,
onSurface: dark ? _onSurfaceDark : _onSurface,
surfaceContainerHighest: dark ? _dividerDark : _divider,
onSurfaceVariant: dark ? _textSecondaryDark : _textSecondary,
);
final textTheme = _textTheme(colorScheme);
final buttonBorder = RoundedRectangleBorder(
borderRadius: BorderRadius.circular(kRadiusInput),
);
return ThemeData(
brightness: brightness,
colorScheme: colorScheme,
scaffoldBackgroundColor: dark ? _backgroundDark : _background,
textTheme: textTheme,
// AppBar:干净(零海拔 + 页面背景同色),标题用 titleLarge。
appBarTheme: AppBarTheme(
backgroundColor: dark ? _backgroundDark : _background,
surfaceTintColor: Colors.transparent,
foregroundColor: colorScheme.onSurface,
titleTextStyle: textTheme.titleLarge,
elevation: 0,
scrolledUnderElevation: 1,
centerTitle: false,
),
// 按钮:主/次操作统一高 48(规格 48-52)、圆角 12。
filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom(
minimumSize: const Size(64, 48),
shape: buttonBorder,
),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
minimumSize: const Size(64, 48),
shape: buttonBorder,
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
minimumSize: const Size(48, 48),
shape: buttonBorder,
),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
minimumSize: const Size(48, 48),
shape: buttonBorder,
),
),
// 输入框:圆角 12,轻分隔线边框,聚焦主色 2px。
inputDecorationTheme: InputDecorationTheme(
border: _inputBorder(colorScheme, colorScheme.outline),
enabledBorder: _inputBorder(
colorScheme,
dark ? _dividerDark : _divider,
),
focusedBorder: _inputBorder(colorScheme, colorScheme.primary, width: 2),
errorBorder: _inputBorder(colorScheme, colorScheme.error),
focusedErrorBorder: _inputBorder(
colorScheme,
colorScheme.error,
width: 2,
),
contentPadding: const EdgeInsets.symmetric(
horizontal: kSpace16,
vertical: kSpace12,
),
),
// 卡片:白色表面、圆角 16、细边框无阴影(app-preview .card 形态)。
cardTheme: CardThemeData(
color: colorScheme.surface,
surfaceTintColor: Colors.transparent,
elevation: 0,
shadowColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(kRadiusCard),
side: BorderSide(color: dark ? _dividerDark : kColorCardBorder),
),
),
// Dialog 圆角 16;BottomSheet 仅顶部圆角 16。
dialogTheme: DialogThemeData(
surfaceTintColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(kRadiusCard),
),
),
bottomSheetTheme: BottomSheetThemeData(
backgroundColor: colorScheme.surface,
surfaceTintColor: Colors.transparent,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(kRadiusCard),
),
),
),
// Snackbar:浮动 + inverseSurface(自动随深浅色),动作色 inversePrimary。
snackBarTheme: SnackBarThemeData(
behavior: SnackBarBehavior.floating,
backgroundColor: colorScheme.inverseSurface,
contentTextStyle: textTheme.bodyMedium?.copyWith(
color: colorScheme.onInverseSurface,
),
actionTextColor: colorScheme.inversePrimary,
),
// 底部导航(app-preview .nav):白底 + 顶部细线 + **无胶囊指示器**;
// 选中 primary 蓝图标/文字,未选次文字色。
navigationBarTheme: NavigationBarThemeData(
backgroundColor: colorScheme.surface,
surfaceTintColor: Colors.transparent,
indicatorColor: Colors.transparent,
height: 68,
iconTheme: WidgetStateProperty.resolveWith(
(states) => IconThemeData(
size: 24,
color: states.contains(WidgetState.selected)
? colorScheme.primary
: (dark ? _textSecondaryDark : kColorTextSecondary),
),
),
labelTextStyle: WidgetStateProperty.resolveWith(
(states) => textTheme.labelMedium!.copyWith(
color: states.contains(WidgetState.selected)
? colorScheme.primary
: (dark ? _textSecondaryDark : kColorTextSecondary),
fontWeight: FontWeight.w500,
),
),
),
// 轻分隔线(不用粗边框)。
dividerTheme: DividerThemeData(
color: dark ? _dividerDark : _divider,
thickness: 1,
space: 1,
),
);
}
static OutlineInputBorder _inputBorder(
ColorScheme scheme,
Color color, {
double width = 1,
}) => OutlineInputBorder(
borderRadius: BorderRadius.circular(kRadiusField),
borderSide: BorderSide(color: color, width: width),
);
/// 字阶(UI-03 §3):24 页面标题 / 17 分组 / 16-14 正文 / 13-12 辅助。
///
/// 只声明 fontSize/字重/颜色,字体族与缩放交给默认排版 + TextScaler
/// (fontSize 随系统字体大小缩放)。
static TextTheme _textTheme(ColorScheme scheme) {
final onSurface = scheme.onSurface;
final onSecondaryText = scheme.onSurfaceVariant;
return TextTheme(
titleLarge: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w600,
color: onSurface,
),
titleMedium: TextStyle(
fontSize: 17,
fontWeight: FontWeight.w600,
color: onSurface,
),
titleSmall: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: onSurface,
),
bodyLarge: TextStyle(fontSize: 16, color: onSurface),
bodyMedium: TextStyle(fontSize: 14, color: onSurface),
bodySmall: TextStyle(fontSize: 13, color: onSecondaryText),
labelLarge: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: onSurface,
),
labelMedium: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: onSurface,
),
labelSmall: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: onSecondaryText,
),
);
}
}