feat: 改用 Material Design 3 标准输入框
- 创建 MaterialInput 和 MaterialPasswordInput 组件 - 移除 shadcn_ui 依赖,使用原生 Material 组件 - 添加圆角边框 + Focus 效果 - 添加浮动标签动画 - 改进登录和注册页面视觉体验 - 统一设计语言(Material Design 3)
This commit is contained in:
230
flutter_monisuo/lib/ui/components/material_input.dart
Normal file
230
flutter_monisuo/lib/ui/components/material_input.dart
Normal file
@@ -0,0 +1,230 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import '../theme/app_color_scheme.dart';
|
||||
import '../theme/app_spacing.dart';
|
||||
import '../theme/app_theme.dart';
|
||||
import '../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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
import 'package:provider/provider.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_extension.dart';
|
||||
import '../../../providers/auth_provider.dart';
|
||||
import '../../components/material_input.dart';
|
||||
import '../main/main_page.dart';
|
||||
import 'register_page.dart';
|
||||
|
||||
@@ -18,14 +18,13 @@ class LoginPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _LoginPageState extends State<LoginPage> {
|
||||
final formKey = GlobalKey<ShadFormState>();
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _usernameController = TextEditingController();
|
||||
final _passwordController = TextEditingController();
|
||||
bool _obscurePassword = true;
|
||||
|
||||
static const _loadingIndicatorSize = 16.0;
|
||||
static const _logoCircleSize = 80.0;
|
||||
static const _inputHeight = 52.0;
|
||||
static const _buttonHeight = 52.0;
|
||||
|
||||
@override
|
||||
@@ -45,8 +44,8 @@ class _LoginPageState extends State<LoginPage> {
|
||||
horizontal: AppSpacing.xl,
|
||||
vertical: AppSpacing.xxl,
|
||||
),
|
||||
child: ShadForm(
|
||||
key: formKey,
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
// 頂部品牌區域
|
||||
@@ -135,63 +134,22 @@ class _LoginPageState extends State<LoginPage> {
|
||||
}
|
||||
|
||||
Widget _buildUsernameField() {
|
||||
return SizedBox(
|
||||
height: _inputHeight,
|
||||
child: ShadInputFormField(
|
||||
id: 'username',
|
||||
placeholder: const Text('請輸入用戶名'),
|
||||
leading: Padding(
|
||||
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,
|
||||
),
|
||||
),
|
||||
return MaterialInput(
|
||||
controller: _usernameController,
|
||||
labelText: '用戶名',
|
||||
hintText: '請輸入用戶名',
|
||||
prefixIcon: Icons.person_outline,
|
||||
validator: _validateUsername,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPasswordField() {
|
||||
final iconColor = context.appColors.onSurfaceMuted;
|
||||
|
||||
return SizedBox(
|
||||
height: _inputHeight,
|
||||
child: ShadInputFormField(
|
||||
id: 'password',
|
||||
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,
|
||||
),
|
||||
),
|
||||
return MaterialPasswordInput(
|
||||
controller: _passwordController,
|
||||
labelText: '密碼',
|
||||
hintText: '請輸入密碼',
|
||||
prefixIcon: Icons.lock_outline,
|
||||
validator: _validatePassword,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -204,13 +162,21 @@ class _LoginPageState extends State<LoginPage> {
|
||||
builder: (context, auth, _) {
|
||||
return SizedBox(
|
||||
height: _buttonHeight,
|
||||
child: ShadButton(
|
||||
child: ElevatedButton(
|
||||
onPressed: auth.isLoading ? null : () => _handleLogin(auth),
|
||||
backgroundColor: buttonColor,
|
||||
foregroundColor: textColor,
|
||||
decoration: ShadDecoration(
|
||||
border: ShadBorder.all(
|
||||
radius: AppRadius.radiusLg,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: buttonColor,
|
||||
foregroundColor: textColor,
|
||||
disabledBackgroundColor: buttonColor.withValues(alpha: 0.5),
|
||||
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
|
||||
@@ -299,12 +265,11 @@ class _LoginPageState extends State<LoginPage> {
|
||||
// ============================================
|
||||
|
||||
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(
|
||||
values['username'],
|
||||
values['password'],
|
||||
_usernameController.text.trim(),
|
||||
_passwordController.text,
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
@@ -331,15 +296,15 @@ class _LoginPageState extends State<LoginPage> {
|
||||
}
|
||||
|
||||
void _showErrorDialog(String message) {
|
||||
showShadDialog(
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => ShadDialog.alert(
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('登錄失敗'),
|
||||
description: Text(message),
|
||||
content: Text(message),
|
||||
actions: [
|
||||
ShadButton(
|
||||
child: const Text('確定'),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('確定'),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -1,26 +1,24 @@
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
|
||||
import '../../../core/theme/app_color_scheme.dart';
|
||||
import '../../../core/theme/app_spacing.dart';
|
||||
import '../../../core/theme/app_theme.dart';
|
||||
import '../../../providers/auth_provider.dart';
|
||||
import '../../components/glass_panel.dart';
|
||||
import '../../components/neon_glow.dart';
|
||||
import '../../components/material_input.dart';
|
||||
import '../main/main_page.dart';
|
||||
|
||||
/// 註冊頁面(兩步註冊:賬號信息 + 身份證上傳)
|
||||
/// 註冊頁面(兩步註冊:賬號信息 + 身份證上傳)
|
||||
class RegisterPage extends StatefulWidget {
|
||||
const RegisterPage({super.key});
|
||||
|
||||
@override
|
||||
State<RegisterPage> createState() => _RegisterPageState();
|
||||
}
|
||||
|
||||
class _RegisterPageState extends State<RegisterPage> {
|
||||
int _currentStep = 0; // 0: 賬號信息, 1: 身份證上傳
|
||||
State<RegisterPage> createState() => _RegisterPageState>();
|
||||
int _currentStep = 0; // 0: 賬號信息, 2: 身份證上傳
|
||||
|
||||
// 第一步
|
||||
final _usernameController = TextEditingController();
|
||||
@@ -84,7 +82,7 @@ class _RegisterPageState extends State<RegisterPage> {
|
||||
backgroundColor: AppColorScheme.darkBackground.withValues(alpha: 0),
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: Icon(LucideIcons.chevronLeft, color: colorScheme.onSurface),
|
||||
icon: Icon(Icons.chevron_left, color: colorScheme.onSurface),
|
||||
onPressed: _currentStep == 1
|
||||
? () => setState(() => _currentStep = 0)
|
||||
: () => Navigator.pop(context),
|
||||
@@ -170,7 +168,7 @@ class _RegisterPageState extends State<RegisterPage> {
|
||||
),
|
||||
child: Center(
|
||||
child: isComplete
|
||||
? Icon(LucideIcons.check, size: 16, color: textColor)
|
||||
? Icon(Icons.check, size: 16, color: textColor)
|
||||
: Text(
|
||||
number,
|
||||
style: AppTextStyles.headlineMedium(context).copyWith(
|
||||
@@ -191,7 +189,7 @@ class _RegisterPageState extends State<RegisterPage> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 第一步:賬號信息
|
||||
/// 第一步:賬號信息
|
||||
Widget _buildStep1(ColorScheme colorScheme) {
|
||||
return Form(
|
||||
key: _formKey,
|
||||
@@ -211,62 +209,40 @@ class _RegisterPageState extends State<RegisterPage> {
|
||||
SizedBox(height: AppSpacing.xxl),
|
||||
|
||||
// 用戶名
|
||||
TextFormField(
|
||||
MaterialInput(
|
||||
controller: _usernameController,
|
||||
style: TextStyle(color: colorScheme.onSurface),
|
||||
decoration: InputDecoration(
|
||||
hintText: '請輸入賬號(4-20位字母數字)',
|
||||
prefixIcon: Icon(Icons.person_outline, color: colorScheme.onSurfaceVariant),
|
||||
),
|
||||
labelText: '賬號',
|
||||
hintText: '請輸入賬號(4-20位字母數字)',
|
||||
prefixIcon: Icons.person_outline,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) return '請輸入賬號';
|
||||
if (value.length < 4) return '賬號至少4位';
|
||||
if (value.length > 20) return '賬號最多20位';
|
||||
if (value.length < 4) return '賬號過短';
|
||||
if (value.length > 20) return '賬號過長';
|
||||
return null;
|
||||
},
|
||||
),
|
||||
SizedBox(height: AppSpacing.md),
|
||||
|
||||
// 密碼
|
||||
TextFormField(
|
||||
MaterialPasswordInput(
|
||||
controller: _passwordController,
|
||||
obscureText: _obscurePassword,
|
||||
style: TextStyle(color: colorScheme.onSurface),
|
||||
decoration: InputDecoration(
|
||||
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),
|
||||
),
|
||||
),
|
||||
labelText: '密碼',
|
||||
hintText: '請輸入密碼(至少6位)',
|
||||
prefixIcon: Icons.lock_outline,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) return '請輸入密碼';
|
||||
if (value.length < 6) return '密碼至少6位';
|
||||
if (value.length < 6) return '密碼過短';
|
||||
return null;
|
||||
},
|
||||
),
|
||||
SizedBox(height: AppSpacing.md),
|
||||
|
||||
// 確認密碼
|
||||
TextFormField(
|
||||
MaterialPasswordInput(
|
||||
controller: _confirmPasswordController,
|
||||
obscureText: _obscureConfirmPassword,
|
||||
style: TextStyle(color: colorScheme.onSurface),
|
||||
decoration: InputDecoration(
|
||||
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),
|
||||
),
|
||||
),
|
||||
labelText: '確認密碼',
|
||||
hintText: '請再次輸入密碼',
|
||||
prefixIcon: Icons.lock_outline,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) return '請再次輸入密碼';
|
||||
if (value != _passwordController.text) return '兩次密碼不一致';
|
||||
@@ -275,14 +251,12 @@ class _RegisterPageState extends State<RegisterPage> {
|
||||
),
|
||||
SizedBox(height: AppSpacing.md),
|
||||
|
||||
// 推廣碼(可選)
|
||||
TextFormField(
|
||||
// 推廣碼(可選)
|
||||
MaterialInput(
|
||||
controller: _referralCodeController,
|
||||
style: TextStyle(color: colorScheme.onSurface),
|
||||
decoration: InputDecoration(
|
||||
hintText: '推廣碼(選填)',
|
||||
prefixIcon: Icon(Icons.card_giftcard, color: colorScheme.onSurfaceVariant),
|
||||
),
|
||||
labelText: '推廣碼',
|
||||
hintText: '推廣碼(選填)',
|
||||
prefixIcon: Icons.card_giftcard,
|
||||
),
|
||||
SizedBox(height: AppSpacing.xl),
|
||||
|
||||
@@ -307,15 +281,17 @@ class _RegisterPageState extends State<RegisterPage> {
|
||||
Center(
|
||||
child: TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text('已有賬號?立即登錄', style: AppTextStyles.headlineMedium(context)),
|
||||
child: Text(
|
||||
'已有賬號?立即登錄',
|
||||
style: AppTextStyles.headlineMedium(context)),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 第二步:身份證上傳
|
||||
/// 第二步:身份證上傳
|
||||
Widget _buildStep2(ColorScheme colorScheme) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
@@ -332,10 +308,10 @@ class _RegisterPageState extends State<RegisterPage> {
|
||||
padding: EdgeInsets.all(AppSpacing.sm),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.primary.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(AppRadius.md),
|
||||
borderRadius: AppRadius.radiusLg,
|
||||
),
|
||||
child: Icon(
|
||||
LucideIcons.shieldCheck,
|
||||
Icons.shield,
|
||||
color: colorScheme.primary,
|
||||
size: 22,
|
||||
),
|
||||
@@ -360,98 +336,73 @@ class _RegisterPageState extends State<RegisterPage> {
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: AppSpacing.xl),
|
||||
SizedBox(height: AppSpacing.xl),
|
||||
|
||||
// 身份證正面
|
||||
Text(
|
||||
'身份證正面(人像面)',
|
||||
style: AppTextStyles.bodyLarge(context).copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: colorScheme.onSurface,
|
||||
// 身份證正面
|
||||
Text(
|
||||
'身份證正面(人像面)',
|
||||
style: AppTextStyles.bodyLarge(context).copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
SizedBox(height: AppSpacing.sm),
|
||||
_buildUploadZone(
|
||||
imageFile: _frontFile,
|
||||
imageBytes: _frontBytes,
|
||||
label: '人像面',
|
||||
onTap: () => _pickImage(true),
|
||||
colorScheme: colorScheme,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: AppSpacing.sm),
|
||||
_buildUploadZone(
|
||||
imageFile: _frontFile,
|
||||
imageBytes: _frontBytes,
|
||||
label: '人像面',
|
||||
onTap: () => _pickImage(true),
|
||||
colorScheme: colorScheme,
|
||||
),
|
||||
SizedBox(height: AppSpacing.lg),
|
||||
|
||||
// 身份證反面
|
||||
Text(
|
||||
'身份證反面(國徽面)',
|
||||
style: AppTextStyles.bodyLarge(context).copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: colorScheme.onSurface,
|
||||
'身份證反面(國徽面)',
|
||||
style: AppTextStyles.bodyLarge(context).copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: colorScheme.onSurface
|
||||
)
|
||||
),
|
||||
SizedBox(height: AppSpacing.sm),
|
||||
_buildUploadZone(
|
||||
imageFile: _backFile,
|
||||
imageBytes: _backBytes,
|
||||
label: '國徽面',
|
||||
onTap: () => _pickImage(false),
|
||||
colorScheme: colorScheme
|
||||
)
|
||||
),
|
||||
),
|
||||
SizedBox(height: AppSpacing.sm),
|
||||
_buildUploadZone(
|
||||
imageFile: _backFile,
|
||||
imageBytes: _backBytes,
|
||||
label: '國徽面',
|
||||
onTap: () => _pickImage(false),
|
||||
colorScheme: colorScheme,
|
||||
),
|
||||
SizedBox(height: AppSpacing.xl),
|
||||
SizedBox(height: AppSpacing.lg),
|
||||
|
||||
// 註冊按鈕
|
||||
Consumer<AuthProvider>(
|
||||
builder: (context, auth, _) {
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
child: NeonButton(
|
||||
text: _isLoading ? '註冊中...' : '完成註冊',
|
||||
type: NeonButtonType.primary,
|
||||
onPressed: _canSubmit && !auth.isLoading ? _handleRegister : null,
|
||||
height: 48,
|
||||
showGlow: _canSubmit,
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
child: NeonButton(
|
||||
text: _isLoading ? '註冊中...' : '完成註冊',
|
||||
type: NeonButtonType.primary,
|
||||
onPressed: _canSubmit && !auth.isLoading ? _handleRegister : null,
|
||||
height: 48,
|
||||
showGlow: _canSubmit,
|
||||
)
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: AppSpacing.lg),
|
||||
|
||||
// 安全提示
|
||||
Container(
|
||||
padding: EdgeInsets.all(AppSpacing.md),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorScheme.up.withValues(alpha: 0.06),
|
||||
borderRadius: AppRadius.radiusLg,
|
||||
border: Border.all(color: AppColorScheme.up.withValues(alpha: 0.12)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(LucideIcons.lock, size: 16, color: AppColorScheme.up),
|
||||
SizedBox(width: AppSpacing.sm),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'您的身份信息將被加密存儲,僅用於身份驗證',
|
||||
style: AppTextStyles.bodySmall(context).copyWith(
|
||||
color: AppColorScheme.up.withValues(alpha: 0.8),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildUploadZone({
|
||||
required XFile? imageFile,
|
||||
required XFile? imageFile;
|
||||
required Uint8List? imageBytes,
|
||||
required String label,
|
||||
required VoidCallback onTap,
|
||||
required String label;
|
||||
required ColorScheme colorScheme,
|
||||
}) {
|
||||
final hasImage = imageFile != null && imageBytes != null;
|
||||
@@ -491,8 +442,10 @@ class _RegisterPageState extends State<RegisterPage> {
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
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(
|
||||
bottomLeft: Radius.circular(AppRadius.xl),
|
||||
bottomRight: Radius.circular(AppRadius.xl),
|
||||
@@ -526,46 +479,77 @@ class _RegisterPageState extends State<RegisterPage> {
|
||||
color: AppColorScheme.darkOnPrimary.withValues(alpha: 0.15),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(LucideIcons.x, size: 14, color: AppColorScheme.darkOnPrimary),
|
||||
child: Icon(Icons.close, size: 14, color: AppColorScheme.darkOnPrimary),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
: CustomPaint(
|
||||
painter: _DashedBorderPainter(
|
||||
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.2),
|
||||
borderRadius: AppRadius.xl,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.camera_alt,
|
||||
size: 28,
|
||||
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.5),
|
||||
),
|
||||
SizedBox(height: AppSpacing.sm),
|
||||
Text(
|
||||
'點擊上傳$label',
|
||||
style: AppTextStyles.bodyLarge(context).copyWith(
|
||||
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
SizedBox(height: AppSpacing.xs),
|
||||
Text(
|
||||
'支持 JPG、PNG 格式',
|
||||
style: AppTextStyles.bodySmall(context).copyWith(
|
||||
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.4),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
],
|
||||
),
|
||||
);
|
||||
)
|
||||
: CustomPaint(
|
||||
painter: _DashedBorderPainter(
|
||||
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.2),
|
||||
borderRadius: AppRadius.xl,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
LucideIcons.camera,
|
||||
size: 28,
|
||||
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.5),
|
||||
),
|
||||
SizedBox(height: AppSpacing.sm),
|
||||
Text(
|
||||
'點擊上傳$label',
|
||||
style: AppTextStyles.bodyLarge(context).copyWith(
|
||||
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
SizedBox(height: AppSpacing.xs),
|
||||
Text(
|
||||
'支持 JPG、PNG 格式',
|
||||
style: AppTextStyles.bodySmall(context).copyWith(
|
||||
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.4),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
painter: _DashedBorderPainter(
|
||||
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.2),
|
||||
borderRadius: AppRadius.xl,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.camera_alt,
|
||||
size: 28,
|
||||
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.5),
|
||||
),
|
||||
SizedBox(height: AppSpacing.sm),
|
||||
Text(
|
||||
'点击上传$label',
|
||||
style: AppTextStyles.bodyLarge(context).copyWith(
|
||||
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
SizedBox(height: AppSpacing.xs),
|
||||
Text(
|
||||
'支持 JPG、PNG 格式',
|
||||
style: AppTextStyles.bodySmall(context).copyWith(
|
||||
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.4),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -579,12 +563,15 @@ class _RegisterPageState extends State<RegisterPage> {
|
||||
final response = await auth.register(
|
||||
_usernameController.text.trim(),
|
||||
_passwordController.text,
|
||||
referralCode: _referralCodeController.text.trim().isEmpty
|
||||
? null
|
||||
referralCode: _referralCodeController.text.trim().isEmpty ? null
|
||||
: _referralCodeController.text.trim(),
|
||||
: referralCode,
|
||||
},
|
||||
frontBytes: _frontBytes!,
|
||||
backBytes: _backBytes!,
|
||||
referralCode: _referralCodeController.text,
|
||||
);
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
@@ -594,15 +581,15 @@ class _RegisterPageState extends State<RegisterPage> {
|
||||
MaterialPageRoute(builder: (_) => const MainPage()),
|
||||
);
|
||||
} else {
|
||||
showShadDialog(
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => ShadDialog.alert(
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('註冊失敗'),
|
||||
description: Text(response.message ?? '請稍後重試'),
|
||||
content: Text(response.message ?? '請稍後重試'),
|
||||
actions: [
|
||||
ShadButton(
|
||||
child: const Text('確定'),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(),
|
||||
child: const Text('確定'),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -610,15 +597,15 @@ class _RegisterPageState extends State<RegisterPage> {
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
showShadDialog(
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => ShadDialog.alert(
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('註冊失敗'),
|
||||
description: Text(e.toString()),
|
||||
content: Text(e.toString()),
|
||||
actions: [
|
||||
ShadButton(
|
||||
child: const Text('確定'),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(),
|
||||
child: const Text('確定'),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -630,7 +617,7 @@ class _RegisterPageState extends State<RegisterPage> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 虛線邊框畫筆
|
||||
/// 虚线边框画筆
|
||||
class _DashedBorderPainter extends CustomPainter {
|
||||
final Color color;
|
||||
final double borderRadius;
|
||||
|
||||
Reference in New Issue
Block a user