diff --git a/apps/lib/core/router/app_router.dart b/apps/lib/core/router/app_router.dart new file mode 100644 index 0000000..f99b199 --- /dev/null +++ b/apps/lib/core/router/app_router.dart @@ -0,0 +1,30 @@ +import 'package:go_router/go_router.dart'; + +import '../../features/auth/ui/screens/login_email_screen.dart'; +import '../../features/auth/ui/screens/login_password_screen.dart'; +import '../../features/auth/ui/screens/login_code_screen.dart'; +import '../../features/auth/ui/screens/register_screen.dart'; +import '../../features/auth/ui/screens/register_step2_screen.dart'; + +final appRouter = GoRouter( + initialLocation: '/', + routes: [ + GoRoute(path: '/', builder: (context, state) => const LoginEmailScreen()), + GoRoute( + path: '/login/password', + builder: (context, state) => const LoginPasswordScreen(), + ), + GoRoute( + path: '/login/code', + builder: (context, state) => const LoginCodeScreen(), + ), + GoRoute( + path: '/register', + builder: (context, state) => const RegisterScreen(), + ), + GoRoute( + path: '/register/step2', + builder: (context, state) => const RegisterStep2Screen(), + ), + ], +); diff --git a/apps/lib/features/auth/ui/screens/login_code_screen.dart b/apps/lib/features/auth/ui/screens/login_code_screen.dart new file mode 100644 index 0000000..59351f1 --- /dev/null +++ b/apps/lib/features/auth/ui/screens/login_code_screen.dart @@ -0,0 +1,182 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import '../../../../core/theme/design_tokens.dart'; +import '../../../../shared/widgets/app_button.dart'; + +class LoginCodeScreen extends StatefulWidget { + const LoginCodeScreen({super.key}); + + @override + State createState() => _LoginCodeScreenState(); +} + +class _LoginCodeScreenState extends State { + final _codeController = TextEditingController(); + + @override + void dispose() { + _codeController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.background, + body: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + _buildAppIcon(), + const SizedBox(height: 24), + _buildAppTitle(), + const SizedBox(height: 32), + _buildFormContainer(), + ], + ), + ), + ), + _buildFooter(), + const SizedBox(height: 24), + ], + ), + ), + ), + ); + } + + Widget _buildAppIcon() { + return Container( + width: 104, + height: 104, + decoration: BoxDecoration( + color: AppColors.appIconRing, + borderRadius: BorderRadius.circular(52), + border: Border.all(color: AppColors.appIconBorder, width: 1), + ), + child: Center( + child: ClipRRect( + borderRadius: BorderRadius.circular(38), + child: Image.asset( + 'assets/images/logo.png', + width: 76, + height: 76, + fit: BoxFit.cover, + ), + ), + ), + ); + } + + Widget _buildAppTitle() { + return const Text( + 'linksy', + style: TextStyle( + fontFamily: 'Playfair Display', + fontSize: 34, + fontWeight: FontWeight.w700, + fontStyle: FontStyle.italic, + color: AppColors.appTitle, + letterSpacing: 0.5, + ), + ); + } + + Widget _buildFormContainer() { + return SizedBox( + width: 327, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '邮箱验证码', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + color: Color(0xFF475569), + ), + ), + const SizedBox(height: 6), + Row( + children: [ + Expanded( + child: SizedBox( + height: 40, + child: TextField( + controller: _codeController, + keyboardType: TextInputType.number, + decoration: const InputDecoration( + hintText: '输入验证码', + contentPadding: EdgeInsets.symmetric( + horizontal: 12, + vertical: 10, + ), + ), + ), + ), + ), + const SizedBox(width: 8), + SizedBox( + width: 112, + height: 40, + child: OutlinedButton( + onPressed: () {}, + style: OutlinedButton.styleFrom( + backgroundColor: AppColors.background, + side: const BorderSide(color: AppColors.input), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + ), + ), + child: const Text( + '发送验证码', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + color: AppColors.slate500, + ), + ), + ), + ), + ], + ), + ], + ), + const SizedBox(height: 12), + AppButton(text: '登录', onPressed: () {}), + const SizedBox(height: 12), + AppButton( + text: '使用密码登录', + isOutlined: true, + onPressed: () => context.pop(), + ), + ], + ), + ); + } + + Widget _buildFooter() { + return GestureDetector( + onTap: () => context.push('/register'), + child: const Text( + '还没有账号?去注册', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: AppColors.slate500, + ), + ), + ); + } +} diff --git a/apps/lib/features/auth/ui/screens/login_email_screen.dart b/apps/lib/features/auth/ui/screens/login_email_screen.dart new file mode 100644 index 0000000..6dc1907 --- /dev/null +++ b/apps/lib/features/auth/ui/screens/login_email_screen.dart @@ -0,0 +1,170 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import '../../../../core/theme/design_tokens.dart'; +import '../../../../shared/widgets/app_button.dart'; +import '../../../../shared/widgets/warning_banner.dart'; +import '../../../../shared/utils/validators.dart'; + +class LoginEmailScreen extends StatefulWidget { + const LoginEmailScreen({super.key}); + + @override + State createState() => _LoginEmailScreenState(); +} + +class _LoginEmailScreenState extends State { + final _emailController = TextEditingController(); + bool _showWarning = false; + String _warningMessage = ''; + + @override + void dispose() { + _emailController.dispose(); + super.dispose(); + } + + void _handleContinue() { + final error = Validators.email(_emailController.text); + if (error != null) { + setState(() { + _showWarning = true; + _warningMessage = error; + }); + return; + } + setState(() { + _showWarning = false; + }); + context.push('/login/password'); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.background, + body: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + _buildAppIcon(), + const SizedBox(height: 24), + _buildAppTitle(), + const SizedBox(height: 32), + _buildFormContainer(), + ], + ), + ), + ), + _buildFooter(), + const SizedBox(height: 24), + ], + ), + ), + ), + ); + } + + Widget _buildAppIcon() { + return Container( + width: 104, + height: 104, + decoration: BoxDecoration( + color: AppColors.appIconRing, + borderRadius: BorderRadius.circular(52), + border: Border.all(color: AppColors.appIconBorder, width: 1), + ), + child: Center( + child: ClipRRect( + borderRadius: BorderRadius.circular(38), + child: Image.asset( + 'assets/images/logo.png', + width: 76, + height: 76, + fit: BoxFit.cover, + ), + ), + ), + ); + } + + Widget _buildAppTitle() { + return const Text( + 'linksy', + style: TextStyle( + fontFamily: 'Playfair Display', + fontSize: 34, + fontWeight: FontWeight.w700, + fontStyle: FontStyle.italic, + color: AppColors.appTitle, + letterSpacing: 0.5, + ), + ); + } + + Widget _buildFormContainer() { + return SizedBox( + width: 327, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '邮箱', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: AppColors.foreground, + ), + ), + const SizedBox(height: 6), + TextField( + controller: _emailController, + keyboardType: TextInputType.emailAddress, + decoration: const InputDecoration(hintText: '请输入邮箱'), + ), + ], + ), + const SizedBox(height: 12), + WarningBanner(message: _warningMessage, visible: _showWarning), + const SizedBox(height: 16), + AppButton(text: '继续', onPressed: _handleContinue), + ], + ), + ); + } + + Widget _buildFooter() { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + GestureDetector( + onTap: () => context.push('/register'), + child: const Text( + '还没有账号?去注册', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: AppColors.slate500, + ), + ), + ), + const SizedBox(height: 12), + const Text( + '隐私政策 | 服务条款', + style: TextStyle(fontSize: 12, color: AppColors.slate400), + ), + ], + ); + } +} diff --git a/apps/lib/features/auth/ui/screens/login_password_screen.dart b/apps/lib/features/auth/ui/screens/login_password_screen.dart new file mode 100644 index 0000000..cbfadc0 --- /dev/null +++ b/apps/lib/features/auth/ui/screens/login_password_screen.dart @@ -0,0 +1,159 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import '../../../../core/theme/design_tokens.dart'; +import '../../../../shared/widgets/app_button.dart'; + +class LoginPasswordScreen extends StatefulWidget { + const LoginPasswordScreen({super.key}); + + @override + State createState() => _LoginPasswordScreenState(); +} + +class _LoginPasswordScreenState extends State { + final _passwordController = TextEditingController(); + bool _obscureText = true; + + @override + void dispose() { + _passwordController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.background, + body: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + _buildAppIcon(), + const SizedBox(height: 24), + _buildAppTitle(), + const SizedBox(height: 32), + _buildFormContainer(), + ], + ), + ), + ), + _buildFooter(), + const SizedBox(height: 24), + ], + ), + ), + ), + ); + } + + Widget _buildAppIcon() { + return Container( + width: 104, + height: 104, + decoration: BoxDecoration( + color: AppColors.appIconRing, + borderRadius: BorderRadius.circular(52), + border: Border.all(color: AppColors.appIconBorder, width: 1), + ), + child: Center( + child: ClipRRect( + borderRadius: BorderRadius.circular(38), + child: Image.asset( + 'assets/images/logo.png', + width: 76, + height: 76, + fit: BoxFit.cover, + ), + ), + ), + ); + } + + Widget _buildAppTitle() { + return const Text( + 'linksy', + style: TextStyle( + fontFamily: 'Playfair Display', + fontSize: 34, + fontWeight: FontWeight.w700, + fontStyle: FontStyle.italic, + color: AppColors.appTitle, + letterSpacing: 0.5, + ), + ); + } + + Widget _buildFormContainer() { + return SizedBox( + width: 327, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '密码', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + color: Color(0xFF475569), + ), + ), + const SizedBox(height: 6), + TextField( + controller: _passwordController, + obscureText: _obscureText, + decoration: InputDecoration( + hintText: '请输入密码', + suffixIcon: IconButton( + icon: Icon( + _obscureText ? Icons.visibility_off : Icons.visibility, + size: 20, + color: AppColors.slate400, + ), + onPressed: () { + setState(() { + _obscureText = !_obscureText; + }); + }, + ), + ), + ), + ], + ), + const SizedBox(height: 12), + AppButton(text: '登录', onPressed: () {}), + const SizedBox(height: 12), + AppButton( + text: '使用验证码登录', + isOutlined: true, + onPressed: () => context.push('/login/code'), + ), + ], + ), + ); + } + + Widget _buildFooter() { + return GestureDetector( + onTap: () => context.push('/register'), + child: const Text( + '还没有账号?去注册', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: AppColors.slate500, + ), + ), + ); + } +} diff --git a/apps/lib/features/auth/ui/screens/register_screen.dart b/apps/lib/features/auth/ui/screens/register_screen.dart new file mode 100644 index 0000000..8a2699d --- /dev/null +++ b/apps/lib/features/auth/ui/screens/register_screen.dart @@ -0,0 +1,180 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import '../../../../core/theme/design_tokens.dart'; +import '../../../../shared/widgets/app_button.dart'; + +class RegisterScreen extends StatefulWidget { + const RegisterScreen({super.key}); + + @override + State createState() => _RegisterScreenState(); +} + +class _RegisterScreenState extends State { + final _nicknameController = TextEditingController(); + final _emailController = TextEditingController(); + + @override + void dispose() { + _nicknameController.dispose(); + _emailController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.background, + body: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + _buildAppIcon(), + const SizedBox(height: 24), + _buildAppTitle(), + const SizedBox(height: 24), + _buildFormContainer(), + ], + ), + ), + ), + _buildFooter(), + const SizedBox(height: 24), + ], + ), + ), + ), + ); + } + + Widget _buildAppIcon() { + return Container( + width: 104, + height: 104, + decoration: BoxDecoration( + color: AppColors.appIconRing, + borderRadius: BorderRadius.circular(52), + border: Border.all(color: AppColors.appIconBorder, width: 1), + ), + child: Center( + child: ClipRRect( + borderRadius: BorderRadius.circular(38), + child: Image.asset( + 'assets/images/logo.png', + width: 76, + height: 76, + fit: BoxFit.cover, + ), + ), + ), + ); + } + + Widget _buildAppTitle() { + return const Text( + 'linksy', + style: TextStyle( + fontFamily: 'Playfair Display', + fontSize: 34, + fontWeight: FontWeight.w700, + fontStyle: FontStyle.italic, + color: AppColors.appTitle, + letterSpacing: 0.5, + ), + ); + } + + Widget _buildFormContainer() { + return SizedBox( + width: 327, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildInput('昵称', '请输入昵称', _nicknameController), + const SizedBox(height: 12), + _buildInput('邮箱', '请输入邮箱', _emailController), + const SizedBox(height: 12), + _buildStepIndicator(), + const SizedBox(height: 12), + AppButton( + text: '下一步', + onPressed: () => context.push('/register/step2'), + ), + ], + ), + ); + } + + Widget _buildInput( + String label, + String hint, + TextEditingController controller, + ) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + color: Color(0xFF475569), + ), + ), + const SizedBox(height: 6), + TextField( + controller: controller, + decoration: InputDecoration(hintText: hint), + ), + ], + ); + } + + Widget _buildStepIndicator() { + return Row( + children: [ + Expanded( + child: Container( + height: 4, + decoration: BoxDecoration( + color: AppColors.primary, + borderRadius: BorderRadius.circular(99), + ), + ), + ), + const SizedBox(width: 6), + Expanded( + child: Container( + height: 4, + decoration: BoxDecoration( + color: const Color(0xFFDCE3EC), + borderRadius: BorderRadius.circular(99), + ), + ), + ), + ], + ); + } + + Widget _buildFooter() { + return GestureDetector( + onTap: () => context.pop(), + child: const Text( + '已有账号?去登录', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: AppColors.slate500, + ), + ), + ); + } +} diff --git a/apps/lib/features/auth/ui/screens/register_step2_screen.dart b/apps/lib/features/auth/ui/screens/register_step2_screen.dart new file mode 100644 index 0000000..0ea3329 --- /dev/null +++ b/apps/lib/features/auth/ui/screens/register_step2_screen.dart @@ -0,0 +1,274 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import '../../../../core/theme/design_tokens.dart'; +import '../../../../shared/widgets/app_button.dart'; + +class RegisterStep2Screen extends StatefulWidget { + const RegisterStep2Screen({super.key}); + + @override + State createState() => _RegisterStep2ScreenState(); +} + +class _RegisterStep2ScreenState extends State { + final _passwordController = TextEditingController(); + final _codeController = TextEditingController(); + final _inviteController = TextEditingController(); + bool _obscureText = true; + + @override + void dispose() { + _passwordController.dispose(); + _codeController.dispose(); + _inviteController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.background, + body: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + _buildAppIcon(), + const SizedBox(height: 24), + _buildAppTitle(), + const SizedBox(height: 24), + _buildFormContainer(), + ], + ), + ), + ), + _buildFooter(), + const SizedBox(height: 24), + ], + ), + ), + ), + ); + } + + Widget _buildAppIcon() { + return Container( + width: 104, + height: 104, + decoration: BoxDecoration( + color: AppColors.appIconRing, + borderRadius: BorderRadius.circular(52), + border: Border.all(color: AppColors.appIconBorder, width: 1), + ), + child: Center( + child: ClipRRect( + borderRadius: BorderRadius.circular(38), + child: Image.asset( + 'assets/images/logo.png', + width: 76, + height: 76, + fit: BoxFit.cover, + ), + ), + ), + ); + } + + Widget _buildAppTitle() { + return const Text( + 'linksy', + style: TextStyle( + fontFamily: 'Playfair Display', + fontSize: 34, + fontWeight: FontWeight.w700, + fontStyle: FontStyle.italic, + color: AppColors.appTitle, + letterSpacing: 0.5, + ), + ); + } + + Widget _buildFormContainer() { + return SizedBox( + width: 327, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildPasswordInput(), + const SizedBox(height: 12), + _buildCodeInput(), + const SizedBox(height: 12), + _buildInviteInput(), + const SizedBox(height: 12), + _buildStepIndicator(), + const SizedBox(height: 12), + AppButton(text: '完成注册', onPressed: () {}), + ], + ), + ); + } + + Widget _buildPasswordInput() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '密码', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + color: Color(0xFF475569), + ), + ), + const SizedBox(height: 6), + TextField( + controller: _passwordController, + obscureText: _obscureText, + decoration: InputDecoration( + hintText: '请输入至少 8 位密码', + suffixIcon: IconButton( + icon: Icon( + _obscureText ? Icons.visibility_off : Icons.visibility, + size: 20, + color: AppColors.slate400, + ), + onPressed: () { + setState(() { + _obscureText = !_obscureText; + }); + }, + ), + ), + ), + ], + ); + } + + Widget _buildCodeInput() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '邮箱验证码', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + color: Color(0xFF475569), + ), + ), + const SizedBox(height: 6), + Row( + children: [ + Expanded( + child: SizedBox( + height: 40, + child: TextField( + controller: _codeController, + keyboardType: TextInputType.number, + decoration: const InputDecoration( + hintText: '输入验证码', + contentPadding: EdgeInsets.symmetric( + horizontal: 12, + vertical: 10, + ), + ), + ), + ), + ), + const SizedBox(width: 8), + SizedBox( + width: 112, + height: 40, + child: OutlinedButton( + onPressed: () {}, + style: OutlinedButton.styleFrom( + backgroundColor: AppColors.background, + side: const BorderSide(color: AppColors.input), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + ), + ), + child: const Text( + '发送验证码', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + color: AppColors.slate500, + ), + ), + ), + ), + ], + ), + ], + ); + } + + Widget _buildInviteInput() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '邀请码(可选)', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + color: Color(0xFF475569), + ), + ), + const SizedBox(height: 6), + TextField( + controller: _inviteController, + decoration: const InputDecoration(hintText: '有邀请码可填写'), + ), + ], + ); + } + + Widget _buildStepIndicator() { + return Row( + children: [ + Expanded( + child: Container( + height: 4, + decoration: BoxDecoration( + color: AppColors.primary, + borderRadius: BorderRadius.circular(99), + ), + ), + ), + const SizedBox(width: 6), + Expanded( + child: Container( + height: 4, + decoration: BoxDecoration( + color: AppColors.primary, + borderRadius: BorderRadius.circular(99), + ), + ), + ), + ], + ); + } + + Widget _buildFooter() { + return GestureDetector( + onTap: () => context.pop(), + child: const Text( + '已有账号?去登录', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: AppColors.slate500, + ), + ), + ); + } +} diff --git a/apps/lib/main.dart b/apps/lib/main.dart new file mode 100644 index 0000000..b7bc359 --- /dev/null +++ b/apps/lib/main.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; +import 'core/theme/app_theme.dart'; +import 'core/router/app_router.dart'; + +void main() { + runApp(const LinksyApp()); +} + +class LinksyApp extends StatelessWidget { + const LinksyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp.router( + title: 'Linksy', + debugShowCheckedModeBanner: false, + theme: AppTheme.light, + routerConfig: appRouter, + ); + } +}