Compare commits

...

3 Commits

Author SHA1 Message Date
e6f5dc1800 fix: 修复 material_input.dart 导入路径 2026-04-08 11:15:13 +08:00
baf2d87e47 fix: 修复 register_page.dart 语法错误 2026-04-08 11:14:27 +08:00
97725cb768 feat: 改用 Material Design 3 标准输入框
- 创建 MaterialInput 和 MaterialPasswordInput 组件
- 移除 shadcn_ui 依赖,使用原生 Material 组件
- 添加圆角边框 + Focus 效果
- 添加浮动标签动画
- 改进登录和注册页面视觉体验
- 统一设计语言(Material Design 3)
2026-04-08 11:11:43 +08:00
3 changed files with 363 additions and 173 deletions

View File

@@ -0,0 +1,230 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../core/theme/app_color_scheme.dart';
import '../../core/theme/app_spacing.dart';
import '../../core/theme/app_theme.dart';
import '../../core/theme/app_theme_extension.dart';
/// Material Design 3 风格的输入框组件
///
/// 特点:
/// - 圆角外边框OutlineInputBorder
/// - Focus 时边框颜色变化
/// - 标签上浮动画
/// - 统一的视觉效果
class MaterialInput extends StatelessWidget {
final TextEditingController? controller;
final String? labelText;
final String? hintText;
final IconData? prefixIcon;
final Widget? suffixIcon;
final bool obscureText;
final TextInputType? keyboardType;
final int? maxLines;
final int? maxLength;
final bool enabled;
final bool readOnly;
final String? Function(String?)? validator;
final ValueChanged<String>? onChanged;
final VoidCallback? onTap;
final ValueChanged<String>? onSubmitted;
final List<TextInputFormatter>? inputFormatters;
final FocusNode? focusNode;
const MaterialInput({
super.key,
this.controller,
this.labelText,
this.hintText,
this.prefixIcon,
this.suffixIcon,
this.obscureText = false,
this.keyboardType,
this.maxLines = 1,
this.maxLength,
this.enabled = true,
this.readOnly = false,
this.validator,
this.onChanged,
this.onTap,
this.onSubmitted,
this.inputFormatters,
this.focusNode,
});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
// Material Design 3 的颜色
final primaryColor = isDark ? AppColorScheme.up : colorScheme.primary;
final borderColor = isDark
? AppColorScheme.darkOutline.withValues(alpha: 0.3)
: colorScheme.outline.withValues(alpha: 0.5);
final fillColor = isDark
? AppColorScheme.darkSurfaceContainerHigh.withValues(alpha: 0.3)
: colorScheme.surfaceContainerHighest.withValues(alpha: 0.5);
return TextFormField(
controller: controller,
focusNode: focusNode,
obscureText: obscureText,
keyboardType: keyboardType,
maxLines: maxLines,
maxLength: maxLength,
enabled: enabled,
readOnly: readOnly,
validator: validator,
onChanged: onChanged,
onTap: onTap,
onFieldSubmitted: onSubmitted,
inputFormatters: inputFormatters,
style: AppTextStyles.headlineMedium(context).copyWith(
color: enabled
? colorScheme.onSurface
: colorScheme.onSurface.withValues(alpha: 0.5),
),
cursorColor: primaryColor,
cursorWidth: 2.0,
cursorHeight: 20,
decoration: InputDecoration(
// 标签Material Design 3 的浮动标签)
labelText: labelText,
labelStyle: AppTextStyles.bodyLarge(context).copyWith(
color: colorScheme.onSurfaceVariant,
),
floatingLabelStyle: AppTextStyles.bodyMedium(context).copyWith(
color: primaryColor,
fontWeight: FontWeight.w600,
),
// 提示文本
hintText: hintText,
hintStyle: AppTextStyles.bodyLarge(context).copyWith(
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.6),
),
// 前置图标
prefixIcon: prefixIcon != null
? Icon(
prefixIcon,
color: colorScheme.onSurfaceVariant,
size: 22,
)
: null,
prefixIconColor: WidgetStateColor.resolveWith((states) {
if (states.contains(WidgetState.focused)) {
return primaryColor;
}
return colorScheme.onSurfaceVariant;
}),
// 后置图标
suffixIcon: suffixIcon,
// 填充颜色
filled: true,
fillColor: fillColor,
// 内容内边距
contentPadding: const EdgeInsets.symmetric(
horizontal: AppSpacing.lg,
vertical: AppSpacing.md,
),
// Material Design 3 边框样式
border: _buildBorder(borderColor, AppRadius.lg),
enabledBorder: _buildBorder(borderColor, AppRadius.lg),
focusedBorder: _buildBorder(primaryColor, AppRadius.lg, width: 2.0),
errorBorder: _buildBorder(colorScheme.error, AppRadius.lg),
focusedErrorBorder: _buildBorder(colorScheme.error, AppRadius.lg, width: 2.0),
disabledBorder: _buildBorder(
borderColor.withValues(alpha: 0.3),
AppRadius.lg,
),
// 错误文本样式
errorStyle: AppTextStyles.bodySmall(context).copyWith(
color: colorScheme.error,
),
// 计数器样式
counterStyle: AppTextStyles.bodySmall(context).copyWith(
color: colorScheme.onSurfaceVariant,
),
),
);
}
OutlineInputBorder _buildBorder(Color color, double radius, {double width = 1.0}) {
return OutlineInputBorder(
borderRadius: BorderRadius.circular(radius),
borderSide: BorderSide(
color: color,
width: width,
),
);
}
}
/// Material Design 3 风格的密码输入框(带显示/隐藏切换)
class MaterialPasswordInput extends StatefulWidget {
final TextEditingController? controller;
final String? labelText;
final String? hintText;
final IconData? prefixIcon;
final String? Function(String?)? validator;
final ValueChanged<String>? onChanged;
final ValueChanged<String>? onSubmitted;
final FocusNode? focusNode;
const MaterialPasswordInput({
super.key,
this.controller,
this.labelText,
this.hintText,
this.prefixIcon,
this.validator,
this.onChanged,
this.onSubmitted,
this.focusNode,
});
@override
State<MaterialPasswordInput> createState() => _MaterialPasswordInputState();
}
class _MaterialPasswordInputState extends State<MaterialPasswordInput> {
bool _obscureText = true;
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return MaterialInput(
controller: widget.controller,
labelText: widget.labelText,
hintText: widget.hintText,
prefixIcon: widget.prefixIcon ?? Icons.lock_outline,
obscureText: _obscureText,
validator: widget.validator,
onChanged: widget.onChanged,
onSubmitted: widget.onSubmitted,
focusNode: widget.focusNode,
suffixIcon: IconButton(
icon: Icon(
_obscureText ? Icons.visibility_off : Icons.visibility,
color: colorScheme.onSurfaceVariant,
size: 22,
),
onPressed: () {
setState(() {
_obscureText = !_obscureText;
});
},
splashRadius: 20,
),
);
}
}

View File

@@ -1,5 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../../../core/theme/app_color_scheme.dart'; import '../../../core/theme/app_color_scheme.dart';
@@ -7,6 +6,7 @@ import '../../../core/theme/app_spacing.dart';
import '../../../core/theme/app_theme.dart'; import '../../../core/theme/app_theme.dart';
import '../../../core/theme/app_theme_extension.dart'; import '../../../core/theme/app_theme_extension.dart';
import '../../../providers/auth_provider.dart'; import '../../../providers/auth_provider.dart';
import '../../components/material_input.dart';
import '../main/main_page.dart'; import '../main/main_page.dart';
import 'register_page.dart'; import 'register_page.dart';
@@ -18,14 +18,13 @@ class LoginPage extends StatefulWidget {
} }
class _LoginPageState extends State<LoginPage> { class _LoginPageState extends State<LoginPage> {
final formKey = GlobalKey<ShadFormState>(); final _formKey = GlobalKey<FormState>();
final _usernameController = TextEditingController(); final _usernameController = TextEditingController();
final _passwordController = TextEditingController(); final _passwordController = TextEditingController();
bool _obscurePassword = true; bool _obscurePassword = true;
static const _loadingIndicatorSize = 16.0; static const _loadingIndicatorSize = 16.0;
static const _logoCircleSize = 80.0; static const _logoCircleSize = 80.0;
static const _inputHeight = 52.0;
static const _buttonHeight = 52.0; static const _buttonHeight = 52.0;
@override @override
@@ -45,8 +44,8 @@ class _LoginPageState extends State<LoginPage> {
horizontal: AppSpacing.xl, horizontal: AppSpacing.xl,
vertical: AppSpacing.xxl, vertical: AppSpacing.xxl,
), ),
child: ShadForm( child: Form(
key: formKey, key: _formKey,
child: Column( child: Column(
children: [ children: [
// 頂部品牌區域 // 頂部品牌區域
@@ -135,63 +134,22 @@ class _LoginPageState extends State<LoginPage> {
} }
Widget _buildUsernameField() { Widget _buildUsernameField() {
return SizedBox( return MaterialInput(
height: _inputHeight, controller: _usernameController,
child: ShadInputFormField( labelText: '用戶名',
id: 'username', hintText: '請輸入用戶名',
placeholder: const Text('請輸入用戶名'), prefixIcon: Icons.person_outline,
leading: Padding( validator: _validateUsername,
padding: const EdgeInsets.only(right: AppSpacing.sm),
child: Icon(LucideIcons.user, size: 18, color: context.appColors.onSurfaceMuted),
),
validator: _validateUsername,
controller: _usernameController,
decoration: ShadDecoration(
border: ShadBorder.all(
color: context.colors.outlineVariant,
radius: AppRadius.radiusLg,
),
),
style: AppTextStyles.headlineMedium(context).copyWith(
color: context.colors.onSurface,
),
),
); );
} }
Widget _buildPasswordField() { Widget _buildPasswordField() {
final iconColor = context.appColors.onSurfaceMuted; return MaterialPasswordInput(
controller: _passwordController,
return SizedBox( labelText: '密碼',
height: _inputHeight, hintText: '請輸入密碼',
child: ShadInputFormField( prefixIcon: Icons.lock_outline,
id: 'password', validator: _validatePassword,
placeholder: const Text('請輸入密碼'),
obscureText: _obscurePassword,
leading: Padding(
padding: const EdgeInsets.only(right: AppSpacing.sm),
child: Icon(LucideIcons.lock, size: 18, color: iconColor),
),
trailing: GestureDetector(
onTap: () => setState(() => _obscurePassword = !_obscurePassword),
child: Icon(
_obscurePassword ? LucideIcons.eyeOff : LucideIcons.eye,
size: 18,
color: iconColor,
),
),
validator: _validatePassword,
controller: _passwordController,
decoration: ShadDecoration(
border: ShadBorder.all(
color: context.colors.outlineVariant,
radius: AppRadius.radiusLg,
),
),
style: AppTextStyles.headlineMedium(context).copyWith(
color: context.colors.onSurface,
),
),
); );
} }
@@ -204,13 +162,21 @@ class _LoginPageState extends State<LoginPage> {
builder: (context, auth, _) { builder: (context, auth, _) {
return SizedBox( return SizedBox(
height: _buttonHeight, height: _buttonHeight,
child: ShadButton( child: ElevatedButton(
onPressed: auth.isLoading ? null : () => _handleLogin(auth), onPressed: auth.isLoading ? null : () => _handleLogin(auth),
backgroundColor: buttonColor, style: ElevatedButton.styleFrom(
foregroundColor: textColor, backgroundColor: buttonColor,
decoration: ShadDecoration( foregroundColor: textColor,
border: ShadBorder.all( disabledBackgroundColor: buttonColor.withValues(alpha: 0.5),
radius: AppRadius.radiusLg, disabledForegroundColor: textColor.withValues(alpha: 0.5),
elevation: 0,
shadowColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppRadius.lg),
),
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.xl,
vertical: AppSpacing.md,
), ),
), ),
child: auth.isLoading child: auth.isLoading
@@ -299,12 +265,11 @@ class _LoginPageState extends State<LoginPage> {
// ============================================ // ============================================
Future<void> _handleLogin(AuthProvider auth) async { Future<void> _handleLogin(AuthProvider auth) async {
if (!formKey.currentState!.saveAndValidate()) return; if (!_formKey.currentState!.validate()) return;
final values = formKey.currentState!.value;
final response = await auth.login( final response = await auth.login(
values['username'], _usernameController.text.trim(),
values['password'], _passwordController.text,
); );
if (!mounted) return; if (!mounted) return;
@@ -331,15 +296,15 @@ class _LoginPageState extends State<LoginPage> {
} }
void _showErrorDialog(String message) { void _showErrorDialog(String message) {
showShadDialog( showDialog(
context: context, context: context,
builder: (context) => ShadDialog.alert( builder: (context) => AlertDialog(
title: const Text('登錄失敗'), title: const Text('登錄失敗'),
description: Text(message), content: Text(message),
actions: [ actions: [
ShadButton( TextButton(
child: const Text('確定'),
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(context).pop(),
child: const Text('確定'),
), ),
], ],
), ),

View File

@@ -1,17 +1,18 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import '../../../core/theme/app_color_scheme.dart'; import '../../../core/theme/app_color_scheme.dart';
import '../../../core/theme/app_spacing.dart'; import '../../../core/theme/app_spacing.dart';
import '../../../core/theme/app_theme.dart'; import '../../../core/theme/app_theme.dart';
import '../../../providers/auth_provider.dart'; import '../../../providers/auth_provider.dart';
import '../../components/glass_panel.dart'; import '../../components/glass_panel.dart';
import '../../components/neon_glow.dart'; import '../../components/neon_glow.dart';
import '../../components/material_input.dart';
import '../main/main_page.dart'; import '../main/main_page.dart';
/// 註冊頁面(兩步註冊賬號信息 + 身份證上傳 /// 註冊頁面(兩步註冊:賬號信息 + 身份證上傳)
class RegisterPage extends StatefulWidget { class RegisterPage extends StatefulWidget {
const RegisterPage({super.key}); const RegisterPage({super.key});
@@ -84,7 +85,7 @@ class _RegisterPageState extends State<RegisterPage> {
backgroundColor: AppColorScheme.darkBackground.withValues(alpha: 0), backgroundColor: AppColorScheme.darkBackground.withValues(alpha: 0),
elevation: 0, elevation: 0,
leading: IconButton( leading: IconButton(
icon: Icon(LucideIcons.chevronLeft, color: colorScheme.onSurface), icon: Icon(Icons.chevron_left, color: colorScheme.onSurface),
onPressed: _currentStep == 1 onPressed: _currentStep == 1
? () => setState(() => _currentStep = 0) ? () => setState(() => _currentStep = 0)
: () => Navigator.pop(context), : () => Navigator.pop(context),
@@ -98,7 +99,7 @@ class _RegisterPageState extends State<RegisterPage> {
children: [ children: [
// 步驟指示器 // 步驟指示器
_buildStepIndicator(colorScheme), _buildStepIndicator(colorScheme),
SizedBox(height: AppSpacing.xl), const SizedBox(height: AppSpacing.xl),
// 內容區 // 內容區
_currentStep == 0 ? _buildStep1(colorScheme) : _buildStep2(colorScheme), _currentStep == 0 ? _buildStep1(colorScheme) : _buildStep2(colorScheme),
@@ -170,7 +171,7 @@ class _RegisterPageState extends State<RegisterPage> {
), ),
child: Center( child: Center(
child: isComplete child: isComplete
? Icon(LucideIcons.check, size: 16, color: textColor) ? Icon(Icons.check, size: 16, color: textColor)
: Text( : Text(
number, number,
style: AppTextStyles.headlineMedium(context).copyWith( style: AppTextStyles.headlineMedium(context).copyWith(
@@ -180,7 +181,7 @@ class _RegisterPageState extends State<RegisterPage> {
), ),
), ),
), ),
SizedBox(height: AppSpacing.xs), const SizedBox(height: AppSpacing.xs),
Text( Text(
label, label,
style: AppTextStyles.bodySmall(context).copyWith( style: AppTextStyles.bodySmall(context).copyWith(
@@ -191,7 +192,7 @@ class _RegisterPageState extends State<RegisterPage> {
); );
} }
/// 第一步賬號信息 /// 第一步:賬號信息
Widget _buildStep1(ColorScheme colorScheme) { Widget _buildStep1(ColorScheme colorScheme) {
return Form( return Form(
key: _formKey, key: _formKey,
@@ -208,83 +209,59 @@ class _RegisterPageState extends State<RegisterPage> {
), ),
), ),
), ),
SizedBox(height: AppSpacing.xxl), const SizedBox(height: AppSpacing.xxl),
// 用戶名 // 用戶名
TextFormField( MaterialInput(
controller: _usernameController, controller: _usernameController,
style: TextStyle(color: colorScheme.onSurface), labelText: '賬號',
decoration: InputDecoration( hintText: '請輸入賬號(4-20位字母數字)',
hintText: '請輸入賬號(4-20位字母數字)', prefixIcon: Icons.person_outline,
prefixIcon: Icon(Icons.person_outline, color: colorScheme.onSurfaceVariant),
),
validator: (value) { validator: (value) {
if (value == null || value.isEmpty) return '請輸入賬號'; if (value == null || value.isEmpty) return '請輸入賬號';
if (value.length < 4) return '賬號至少4位'; if (value.length < 4) return '賬號過短';
if (value.length > 20) return '賬號最多20位'; if (value.length > 20) return '賬號過長';
return null; return null;
}, },
), ),
SizedBox(height: AppSpacing.md), const SizedBox(height: AppSpacing.md),
// 密碼 // 密碼
TextFormField( MaterialPasswordInput(
controller: _passwordController, controller: _passwordController,
obscureText: _obscurePassword, labelText: '密碼',
style: TextStyle(color: colorScheme.onSurface), hintText: '請輸入密碼(至少6位)',
decoration: InputDecoration( prefixIcon: Icons.lock_outline,
hintText: '請輸入密碼(至少6位)',
prefixIcon: Icon(Icons.lock_outline, color: colorScheme.onSurfaceVariant),
suffixIcon: IconButton(
icon: Icon(
_obscurePassword ? Icons.visibility_off : Icons.visibility,
color: colorScheme.onSurfaceVariant,
),
onPressed: () => setState(() => _obscurePassword = !_obscurePassword),
),
),
validator: (value) { validator: (value) {
if (value == null || value.isEmpty) return '請輸入密碼'; if (value == null || value.isEmpty) return '請輸入密碼';
if (value.length < 6) return '密碼至少6位'; if (value.length < 6) return '密碼過短';
return null; return null;
}, },
), ),
SizedBox(height: AppSpacing.md), const SizedBox(height: AppSpacing.md),
// 確認密碼 // 確認密碼
TextFormField( MaterialPasswordInput(
controller: _confirmPasswordController, controller: _confirmPasswordController,
obscureText: _obscureConfirmPassword, labelText: '確認密碼',
style: TextStyle(color: colorScheme.onSurface), hintText: '請再次輸入密碼',
decoration: InputDecoration( prefixIcon: Icons.lock_outline,
hintText: '請再次輸入密碼',
prefixIcon: Icon(Icons.lock_outline, color: colorScheme.onSurfaceVariant),
suffixIcon: IconButton(
icon: Icon(
_obscureConfirmPassword ? Icons.visibility_off : Icons.visibility,
color: colorScheme.onSurfaceVariant,
),
onPressed: () => setState(() => _obscureConfirmPassword = !_obscureConfirmPassword),
),
),
validator: (value) { validator: (value) {
if (value == null || value.isEmpty) return '請再次輸入密碼'; if (value == null || value.isEmpty) return '請再次輸入密碼';
if (value != _passwordController.text) return '兩次密碼不一致'; if (value != _passwordController.text) return '兩次密碼不一致';
return null; return null;
}, },
), ),
SizedBox(height: AppSpacing.md), const SizedBox(height: AppSpacing.md),
// 推廣碼(可選 // 推廣碼(可選)
TextFormField( MaterialInput(
controller: _referralCodeController, controller: _referralCodeController,
style: TextStyle(color: colorScheme.onSurface), labelText: '推廣碼',
decoration: InputDecoration( hintText: '推廣碼(選填)',
hintText: '推廣碼(選填)', prefixIcon: Icons.card_giftcard,
prefixIcon: Icon(Icons.card_giftcard, color: colorScheme.onSurfaceVariant),
),
), ),
SizedBox(height: AppSpacing.xl), const SizedBox(height: AppSpacing.xl),
// 下一步按鈕 // 下一步按鈕
SizedBox( SizedBox(
@@ -301,13 +278,16 @@ class _RegisterPageState extends State<RegisterPage> {
showGlow: true, showGlow: true,
), ),
), ),
SizedBox(height: AppSpacing.md), const SizedBox(height: AppSpacing.md),
// 登錄鏈接 // 登錄鏈接
Center( Center(
child: TextButton( child: TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
child: Text('已有賬號?立即登錄', style: AppTextStyles.headlineMedium(context)), child: Text(
'已有賬號?立即登錄',
style: AppTextStyles.headlineMedium(context),
),
), ),
), ),
], ],
@@ -315,32 +295,32 @@ class _RegisterPageState extends State<RegisterPage> {
); );
} }
/// 第二步身份證上傳 /// 第二步:身份證上傳
Widget _buildStep2(ColorScheme colorScheme) { Widget _buildStep2(ColorScheme colorScheme) {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
// 標題區 // 標題區
GlassPanel( GlassPanel(
padding: EdgeInsets.all(AppSpacing.lg), padding: const EdgeInsets.all(AppSpacing.lg),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( Row(
children: [ children: [
Container( Container(
padding: EdgeInsets.all(AppSpacing.sm), padding: const EdgeInsets.all(AppSpacing.sm),
decoration: BoxDecoration( decoration: BoxDecoration(
color: colorScheme.primary.withValues(alpha: 0.1), color: colorScheme.primary.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(AppRadius.md), borderRadius: BorderRadius.circular(AppRadius.md),
), ),
child: Icon( child: Icon(
LucideIcons.shieldCheck, Icons.shield,
color: colorScheme.primary, color: colorScheme.primary,
size: 22, size: 22,
), ),
), ),
SizedBox(width: AppSpacing.md), const SizedBox(width: AppSpacing.md),
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@@ -351,7 +331,7 @@ class _RegisterPageState extends State<RegisterPage> {
color: colorScheme.onSurface, color: colorScheme.onSurface,
), ),
), ),
SizedBox(height: AppSpacing.xs), const SizedBox(height: AppSpacing.xs),
Text( Text(
'上傳身份證正反面完成註冊', '上傳身份證正反面完成註冊',
style: AppTextStyles.bodyMedium(context).copyWith( style: AppTextStyles.bodyMedium(context).copyWith(
@@ -362,17 +342,17 @@ class _RegisterPageState extends State<RegisterPage> {
), ),
], ],
), ),
SizedBox(height: AppSpacing.xl), const SizedBox(height: AppSpacing.xl),
// 身份證正面 // 身份證正面
Text( Text(
'身份證正面人像面', '身份證正面(人像面)',
style: AppTextStyles.bodyLarge(context).copyWith( style: AppTextStyles.bodyLarge(context).copyWith(
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: colorScheme.onSurface, color: colorScheme.onSurface,
), ),
), ),
SizedBox(height: AppSpacing.sm), const SizedBox(height: AppSpacing.sm),
_buildUploadZone( _buildUploadZone(
imageFile: _frontFile, imageFile: _frontFile,
imageBytes: _frontBytes, imageBytes: _frontBytes,
@@ -380,17 +360,17 @@ class _RegisterPageState extends State<RegisterPage> {
onTap: () => _pickImage(true), onTap: () => _pickImage(true),
colorScheme: colorScheme, colorScheme: colorScheme,
), ),
SizedBox(height: AppSpacing.lg), const SizedBox(height: AppSpacing.lg),
// 身份證反面 // 身份證反面
Text( Text(
'身份證反面國徽面', '身份證反面(國徽面)',
style: AppTextStyles.bodyLarge(context).copyWith( style: AppTextStyles.bodyLarge(context).copyWith(
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: colorScheme.onSurface, color: colorScheme.onSurface,
), ),
), ),
SizedBox(height: AppSpacing.sm), const SizedBox(height: AppSpacing.sm),
_buildUploadZone( _buildUploadZone(
imageFile: _backFile, imageFile: _backFile,
imageBytes: _backBytes, imageBytes: _backBytes,
@@ -398,7 +378,7 @@ class _RegisterPageState extends State<RegisterPage> {
onTap: () => _pickImage(false), onTap: () => _pickImage(false),
colorScheme: colorScheme, colorScheme: colorScheme,
), ),
SizedBox(height: AppSpacing.xl), const SizedBox(height: AppSpacing.xl),
// 註冊按鈕 // 註冊按鈕
Consumer<AuthProvider>( Consumer<AuthProvider>(
@@ -408,7 +388,9 @@ class _RegisterPageState extends State<RegisterPage> {
child: NeonButton( child: NeonButton(
text: _isLoading ? '註冊中...' : '完成註冊', text: _isLoading ? '註冊中...' : '完成註冊',
type: NeonButtonType.primary, type: NeonButtonType.primary,
onPressed: _canSubmit && !auth.isLoading ? _handleRegister : null, onPressed: _canSubmit && !auth.isLoading
? _handleRegister
: null,
height: 48, height: 48,
showGlow: _canSubmit, showGlow: _canSubmit,
), ),
@@ -418,20 +400,22 @@ class _RegisterPageState extends State<RegisterPage> {
], ],
), ),
), ),
SizedBox(height: AppSpacing.lg), const SizedBox(height: AppSpacing.lg),
// 安全提示 // 安全提示
Container( Container(
padding: EdgeInsets.all(AppSpacing.md), padding: const EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppColorScheme.up.withValues(alpha: 0.06), color: AppColorScheme.up.withValues(alpha: 0.06),
borderRadius: AppRadius.radiusLg, borderRadius: AppRadius.radiusLg,
border: Border.all(color: AppColorScheme.up.withValues(alpha: 0.12)), border: Border.all(
color: AppColorScheme.up.withValues(alpha: 0.12),
),
), ),
child: Row( child: Row(
children: [ children: [
Icon(LucideIcons.lock, size: 16, color: AppColorScheme.up), Icon(Icons.lock, size: 16, color: AppColorScheme.up),
SizedBox(width: AppSpacing.sm), const SizedBox(width: AppSpacing.sm),
Expanded( Expanded(
child: Text( child: Text(
'您的身份信息將被加密存儲,僅用於身份驗證', '您的身份信息將被加密存儲,僅用於身份驗證',
@@ -468,7 +452,9 @@ class _RegisterPageState extends State<RegisterPage> {
: colorScheme.surfaceContainerHigh.withValues(alpha: 0.3), : colorScheme.surfaceContainerHigh.withValues(alpha: 0.3),
borderRadius: AppRadius.radiusXl, borderRadius: AppRadius.radiusXl,
border: Border.all( border: Border.all(
color: hasImage ? AppColorScheme.up.withValues(alpha: 0.3) : AppColorScheme.darkBackground.withValues(alpha: 0), color: hasImage
? AppColorScheme.up.withValues(alpha: 0.3)
: Colors.transparent,
), ),
), ),
child: hasImage child: hasImage
@@ -483,7 +469,7 @@ class _RegisterPageState extends State<RegisterPage> {
right: 0, right: 0,
bottom: 0, bottom: 0,
child: Container( child: Container(
padding: EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
vertical: AppSpacing.sm, vertical: AppSpacing.sm,
horizontal: AppSpacing.md, horizontal: AppSpacing.md,
), ),
@@ -491,9 +477,13 @@ class _RegisterPageState extends State<RegisterPage> {
gradient: LinearGradient( gradient: LinearGradient(
begin: Alignment.topCenter, begin: Alignment.topCenter,
end: Alignment.bottomCenter, end: Alignment.bottomCenter,
colors: [AppColorScheme.darkBackground.withValues(alpha: 0), AppColorScheme.darkSurfaceLowest.withValues(alpha: 0.6)], colors: [
AppColorScheme.darkBackground.withValues(alpha: 0),
AppColorScheme.darkSurfaceLowest
.withValues(alpha: 0.6),
],
), ),
borderRadius: BorderRadius.only( borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(AppRadius.xl), bottomLeft: Radius.circular(AppRadius.xl),
bottomRight: Radius.circular(AppRadius.xl), bottomRight: Radius.circular(AppRadius.xl),
), ),
@@ -521,12 +511,17 @@ class _RegisterPageState extends State<RegisterPage> {
}); });
}, },
child: Container( child: Container(
padding: EdgeInsets.all(AppSpacing.xs), padding: const EdgeInsets.all(AppSpacing.xs),
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppColorScheme.darkOnPrimary.withValues(alpha: 0.15), color: AppColorScheme.darkOnPrimary
.withValues(alpha: 0.15),
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
child: Icon(LucideIcons.x, size: 14, color: AppColorScheme.darkOnPrimary), child: Icon(
Icons.close,
size: 14,
color: AppColorScheme.darkOnPrimary,
),
), ),
), ),
], ],
@@ -545,18 +540,18 @@ class _RegisterPageState extends State<RegisterPage> {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Icon( Icon(
LucideIcons.camera, Icons.camera_alt,
size: 28, size: 28,
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.5), color: colorScheme.onSurfaceVariant.withValues(alpha: 0.5),
), ),
SizedBox(height: AppSpacing.sm), const SizedBox(height: AppSpacing.sm),
Text( Text(
'點擊上傳$label', '點擊上傳$label',
style: AppTextStyles.bodyLarge(context).copyWith( style: AppTextStyles.bodyLarge(context).copyWith(
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.6), color: colorScheme.onSurfaceVariant.withValues(alpha: 0.6),
), ),
), ),
SizedBox(height: AppSpacing.xs), const SizedBox(height: AppSpacing.xs),
Text( Text(
'支持 JPG、PNG 格式', '支持 JPG、PNG 格式',
style: AppTextStyles.bodySmall(context).copyWith( style: AppTextStyles.bodySmall(context).copyWith(
@@ -594,15 +589,15 @@ class _RegisterPageState extends State<RegisterPage> {
MaterialPageRoute(builder: (_) => const MainPage()), MaterialPageRoute(builder: (_) => const MainPage()),
); );
} else { } else {
showShadDialog( showDialog(
context: context, context: context,
builder: (ctx) => ShadDialog.alert( builder: (ctx) => AlertDialog(
title: const Text('註冊失敗'), title: const Text('註冊失敗'),
description: Text(response.message ?? '請稍後重試'), content: Text(response.message ?? '請稍後重試'),
actions: [ actions: [
ShadButton( TextButton(
child: const Text('確定'),
onPressed: () => Navigator.of(ctx).pop(), onPressed: () => Navigator.of(ctx).pop(),
child: const Text('確定'),
), ),
], ],
), ),
@@ -610,15 +605,15 @@ class _RegisterPageState extends State<RegisterPage> {
} }
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
showShadDialog( showDialog(
context: context, context: context,
builder: (ctx) => ShadDialog.alert( builder: (ctx) => AlertDialog(
title: const Text('註冊失敗'), title: const Text('註冊失敗'),
description: Text(e.toString()), content: Text(e.toString()),
actions: [ actions: [
ShadButton( TextButton(
child: const Text('確定'),
onPressed: () => Navigator.of(ctx).pop(), onPressed: () => Navigator.of(ctx).pop(),
child: const Text('確定'),
), ),
], ],
), ),
@@ -630,7 +625,7 @@ class _RegisterPageState extends State<RegisterPage> {
} }
} }
/// 虛線邊框畫 /// 虚线边框画
class _DashedBorderPainter extends CustomPainter { class _DashedBorderPainter extends CustomPainter {
final Color color; final Color color;
final double borderRadius; final double borderRadius;
@@ -647,8 +642,8 @@ class _DashedBorderPainter extends CustomPainter {
..strokeWidth = 1.5 ..strokeWidth = 1.5
..style = PaintingStyle.stroke; ..style = PaintingStyle.stroke;
final dashWidth = 6.0; const dashWidth = 6.0;
final dashSpace = 4.0; const dashSpace = 4.0;
final path = Path() final path = Path()
..addRRect(RRect.fromRectAndRadius( ..addRRect(RRect.fromRectAndRadius(