diff --git a/.gitignore b/.gitignore index 656bc15..d473ae3 100644 --- a/.gitignore +++ b/.gitignore @@ -14,8 +14,9 @@ dist/ downloads/ eggs/ .eggs/ -lib/ -lib64/ +/lib/ +/lib64/ +!apps/lib/ parts/ sdist/ var/ diff --git a/apps/lib/core/theme/app_theme.dart b/apps/lib/core/theme/app_theme.dart new file mode 100644 index 0000000..f1da0cb --- /dev/null +++ b/apps/lib/core/theme/app_theme.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'design_tokens.dart'; + +class AppTheme { + AppTheme._(); + + static ThemeData get light => ThemeData( + useMaterial3: true, + brightness: Brightness.light, + scaffoldBackgroundColor: AppColors.background, + colorScheme: ColorScheme.fromSeed( + seedColor: AppColors.primary, + brightness: Brightness.light, + ), + fontFamily: 'Inter', + appBarTheme: const AppBarTheme( + backgroundColor: AppColors.background, + foregroundColor: AppColors.slate900, + elevation: 0, + ), + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.primary, + foregroundColor: AppColors.primaryForeground, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppRadius.sm), + ), + ), + ), + inputDecorationTheme: InputDecorationTheme( + filled: true, + fillColor: AppColors.background, + contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppRadius.sm), + borderSide: const BorderSide(color: AppColors.input), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppRadius.sm), + borderSide: const BorderSide(color: AppColors.input), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppRadius.sm), + borderSide: const BorderSide(color: AppColors.input), + ), + hintStyle: const TextStyle( + color: AppColors.mutedForeground, + fontSize: 14, + ), + ), + ); +} diff --git a/apps/lib/core/theme/design_tokens.dart b/apps/lib/core/theme/design_tokens.dart new file mode 100644 index 0000000..373f4c1 --- /dev/null +++ b/apps/lib/core/theme/design_tokens.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; + +class AppColors { + AppColors._(); + + static const primary = Color(0xFF171717); + static const primaryForeground = Color(0xFFFAFAFA); + static const background = Color(0xFFFAFAFA); + static const foreground = Color(0xFF0A0A0A); + static const mutedForeground = Color(0xFF737373); + static const input = Color(0xFFE5E5E5); + static const border = Color(0xFFE5E5E5); + static const white = Color(0xFFFFFFFF); + static const card = Color(0xFFFAFAFA); + + static const slate900 = Color(0xFF0F172A); + static const slate700 = Color(0xFF334155); + static const slate600 = Color(0xFF475569); + static const slate500 = Color(0xFF64748B); + static const slate400 = Color(0xFF94A3B8); + static const slate300 = Color(0xFFCBD5E1); + + static const blue600 = Color(0xFF2563EB); + static const blue500 = Color(0xFF3B82F6); + static const blue400 = Color(0xFF60A5FA); + static const blue100 = Color(0xFFDBEAFE); + static const blue50 = Color(0xFFEFF6FF); + + static const red600 = Color(0xFFDC2626); + static const red500 = Color(0xFFEF4444); + + static const amber600 = Color(0xFFD97706); + static const amber500 = Color(0xFFF59E0B); + + static const emerald600 = Color(0xFF059669); + static const emerald500 = Color(0xFF10B981); + + static const violet600 = Color(0xFF7C3AED); + static const violet500 = Color(0xFF8B5CF6); + + static const warningBackground = Color(0xFFFEF3C7); + static const warningText = Color(0xFF92400E); + + static const appIconRing = Color(0xFFE8F3FF); + static const appIconBorder = Color(0xFFC7DDFB); + static const appTitle = Color(0xFF1E293B); +} + +class AppSpacing { + AppSpacing._(); + + static const double xs = 4.0; + static const double sm = 8.0; + static const double md = 12.0; + static const double lg = 16.0; + static const double xl = 20.0; + static const double xxl = 24.0; +} + +class AppRadius { + AppRadius._(); + + static const double sm = 6.0; + static const double md = 12.0; + static const double lg = 16.0; + static const double xl = 18.0; + static const double xxl = 24.0; + static const double full = 999.0; +} diff --git a/apps/lib/shared/utils/validators.dart b/apps/lib/shared/utils/validators.dart new file mode 100644 index 0000000..640a74c --- /dev/null +++ b/apps/lib/shared/utils/validators.dart @@ -0,0 +1,41 @@ +class Validators { + Validators._(); + + static String? email(String? value) { + if (value == null || value.isEmpty) { + return '请输入邮箱'; + } + final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$'); + if (!emailRegex.hasMatch(value)) { + return '请输入有效的邮箱地址'; + } + return null; + } + + static String? password(String? value) { + if (value == null || value.isEmpty) { + return '请输入密码'; + } + if (value.length < 8) { + return '密码至少需要8位'; + } + return null; + } + + static String? required(String? value, [String? fieldName]) { + if (value == null || value.isEmpty) { + return '请输入${fieldName ?? '内容'}'; + } + return null; + } + + static String? nickname(String? value) { + if (value == null || value.isEmpty) { + return '请输入昵称'; + } + if (value.length < 2) { + return '昵称至少需要2个字符'; + } + return null; + } +} diff --git a/apps/lib/shared/widgets/app_button.dart b/apps/lib/shared/widgets/app_button.dart new file mode 100644 index 0000000..d539cdc --- /dev/null +++ b/apps/lib/shared/widgets/app_button.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; +import '../../core/theme/design_tokens.dart'; + +class AppButton extends StatelessWidget { + final String text; + final VoidCallback? onPressed; + final bool isPrimary; + final bool isOutlined; + final double height; + final bool isLoading; + + const AppButton({ + super.key, + required this.text, + this.onPressed, + this.isPrimary = true, + this.isOutlined = false, + this.height = 44, + this.isLoading = false, + }); + + @override + Widget build(BuildContext context) { + if (isOutlined) { + return SizedBox( + height: height, + child: OutlinedButton( + onPressed: isLoading ? null : onPressed, + style: OutlinedButton.styleFrom( + backgroundColor: AppColors.background, + foregroundColor: AppColors.slate500, + side: const BorderSide(color: AppColors.input), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppRadius.sm), + ), + ), + child: isLoading + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : Text( + text, + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + ), + ), + ), + ); + } + + return SizedBox( + height: height, + child: ElevatedButton( + onPressed: isLoading ? null : onPressed, + child: isLoading + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + color: AppColors.primaryForeground, + ), + ) + : Text( + text, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ), + ); + } +} diff --git a/apps/lib/shared/widgets/app_input.dart b/apps/lib/shared/widgets/app_input.dart new file mode 100644 index 0000000..34ca7e7 --- /dev/null +++ b/apps/lib/shared/widgets/app_input.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import '../../core/theme/design_tokens.dart'; + +class AppInput extends StatelessWidget { + final String label; + final String hint; + final TextEditingController? controller; + final bool obscureText; + final TextInputType? keyboardType; + final Widget? suffix; + final int? maxLines; + final bool enabled; + + const AppInput({ + super.key, + required this.label, + required this.hint, + this.controller, + this.obscureText = false, + this.keyboardType, + this.suffix, + this.maxLines = 1, + this.enabled = true, + }); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + color: AppColors.slate600, + ), + ), + const SizedBox(height: 6), + TextField( + controller: controller, + obscureText: obscureText, + keyboardType: keyboardType, + maxLines: maxLines, + enabled: enabled, + decoration: InputDecoration(hintText: hint, suffixIcon: suffix), + ), + ], + ); + } +} diff --git a/apps/lib/shared/widgets/page_header.dart b/apps/lib/shared/widgets/page_header.dart new file mode 100644 index 0000000..6d9f873 --- /dev/null +++ b/apps/lib/shared/widgets/page_header.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; + +class PageHeader extends StatelessWidget { + final Widget? leading; + final Widget? trailing; + final double height; + + const PageHeader({super.key, this.leading, this.trailing, this.height = 64}); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: height, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + leading ?? const SizedBox.shrink(), + trailing ?? const SizedBox.shrink(), + ], + ), + ), + ); + } +} + +class BackButton extends StatelessWidget { + final VoidCallback? onPressed; + + const BackButton({super.key, this.onPressed}); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onPressed ?? () => Navigator.of(context).pop(), + child: Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: const Color(0xFFF8FAFF), + borderRadius: BorderRadius.circular(18), + border: Border.all(color: const Color(0xFFDEE7F6)), + ), + child: const Icon( + Icons.chevron_left, + size: 18, + color: Color(0xFF334155), + ), + ), + ); + } +} diff --git a/apps/lib/shared/widgets/warning_banner.dart b/apps/lib/shared/widgets/warning_banner.dart new file mode 100644 index 0000000..86c18c5 --- /dev/null +++ b/apps/lib/shared/widgets/warning_banner.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import '../../core/theme/design_tokens.dart'; + +class WarningBanner extends StatelessWidget { + final String message; + final bool visible; + + const WarningBanner({super.key, required this.message, this.visible = true}); + + @override + Widget build(BuildContext context) { + if (!visible) return const SizedBox.shrink(); + + return Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + decoration: BoxDecoration( + color: AppColors.warningBackground, + borderRadius: BorderRadius.circular(AppRadius.sm), + ), + child: Row( + children: [ + const Icon( + Icons.warning_amber_rounded, + size: 16, + color: AppColors.warningText, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + message, + style: const TextStyle( + fontSize: 13, + color: AppColors.warningText, + ), + ), + ), + ], + ), + ); + } +}