phone_login_view.dart 15.8 KB
import 'package:doublefeel_flutter/core/theme/app_theme.dart';
import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';

import '../controllers/login_controller.dart';

class PhoneLoginView extends GetView<LoginController> {
  const PhoneLoginView({super.key});

  @override
  Widget build(BuildContext context) {
    final l10n = context.l10n;
    return AnnotatedRegion<SystemUiOverlayStyle>(
      value: const SystemUiOverlayStyle(
        statusBarColor: Colors.transparent,
        statusBarIconBrightness: Brightness.dark,
        statusBarBrightness: Brightness.light,
        systemNavigationBarColor: Colors.transparent,
        systemNavigationBarIconBrightness: Brightness.dark,
      ),
      child: Scaffold(
        resizeToAvoidBottomInset: false,
        backgroundColor: Colors.white,
        // body: DecoratedBox(
        //   // decoration: const BoxDecoration(
        //   //   gradient: LinearGradient(
        //   //     begin: Alignment.topCenter,
        //   //     end: Alignment.bottomCenter,
        //   //     // stops: [0.0, 0.25, 0.75, 1.0],
        //   //     colors: [
        //   //       Color(0xFFE6DBFF),
        //   //       Color(0xFFEDE4FF),
        //   //       Color(0xFFEEE6FF),
        //   //       Color(0xFFF9F6FF),
        //   //     ],
        //   //   ),
        //   ),
        body: Stack(
          fit: StackFit.expand,
          children: [
            SingleChildScrollView(
              physics: const ClampingScrollPhysics(),
              padding: EdgeInsets.only(
                bottom: MediaQuery.viewInsetsOf(context).bottom + 24,
              ),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.stretch,
                children: [
                  Stack(
                    children: [
                      Image.asset(
                        'assets/images/user_onboarding/bg_phone_login_top.png',
                        fit: BoxFit.fitWidth,
                      ),
                      Positioned(
                        bottom: 67,
                        left: 32,
                        child: Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: [
                            Text(
                              l10n.phoneLoginHello,
                              style: TextStyle(
                                color: Colors.white,
                                fontSize: 28,
                                fontWeight: FontWeight.w900,
                              ),
                            ),
                            const SizedBox(height: 4),
                            Text(
                              l10n.phoneLoginWelcome,
                              style: TextStyle(
                                color: Colors.white,
                                fontSize: 16,
                                fontWeight: FontWeight.w600,
                              ),
                            ),
                          ],
                        ),
                      )
                    ],
                  ),

                  const SizedBox(height: 11),

                  // 输入区
                  Padding(
                    padding: const EdgeInsets.symmetric(horizontal: 28),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.stretch,
                      children: [
                        // 手机号输入框
                        Obx(() {
                          final phoneNotEmpty =
                              controller.phoneInput.value.isNotEmpty;
                          return TextField(
                            controller: controller.phoneController,
                            keyboardType: TextInputType.phone,
                            maxLength: 13,
                            inputFormatters: [
                              FilteringTextInputFormatter.allow(
                                  RegExp(r'[0-9\s]')),
                              _PhoneTextInputFormatter(),
                            ],
                            style: TextStyle(
                              color: context.colors.textPrimary,
                              fontSize: 16,
                              fontWeight: FontWeight.w400,
                            ),
                            decoration: InputDecoration(
                              hintText: l10n.phoneLoginPhoneHint,
                              hintStyle: TextStyle(
                                color: context.colors.textTertiary,
                                fontSize: 16,
                              ),
                              counterText: '',
                              filled: true,
                              fillColor: Color(0xFFF3F3F3),
                              contentPadding: const EdgeInsets.symmetric(
                                horizontal: 20,
                                vertical: 16,
                              ),
                              border: OutlineInputBorder(
                                borderRadius: BorderRadius.circular(27),
                                borderSide: BorderSide.none,
                              ),
                              enabledBorder: OutlineInputBorder(
                                borderRadius: BorderRadius.circular(27),
                                borderSide: BorderSide.none,
                              ),
                              focusedBorder: OutlineInputBorder(
                                borderRadius: BorderRadius.circular(27),
                                borderSide: BorderSide(
                                  color: context.colors.primary,
                                  width: 1,
                                ),
                              ),
                              suffixIcon: phoneNotEmpty
                                  ? GestureDetector(
                                      onTap: controller.phoneController.clear,
                                      child: Icon(
                                        Icons.cancel,
                                        color: context.colors.textTertiary,
                                        size: 18,
                                      ),
                                    )
                                  : null,
                            ),
                          );
                        }),

                        const SizedBox(height: 16),

                        // 验证码输入框(内嵌发送按钮)
                        Obx(() {
                          final countdown = controller.countdownSeconds.value;
                          final isSending = controller.isSendingCode.value;
                          final isCounting = countdown > 0;
                          final hasSent = controller.hasSentCode.value;

                          String btnText = hasSent
                              ? l10n.phoneLoginResend
                              : l10n.phoneLoginSendCode;
                          if (isSending) {
                            btnText = l10n.phoneLoginSending;
                          } else if (isCounting) {
                            btnText = l10n.phoneLoginSentCountdown(countdown);
                          }

                          return TextField(
                            controller: controller.codeController,
                            focusNode: controller.codeFocusNode,
                            keyboardType: TextInputType.text,
                            maxLength: 8,
                            style: TextStyle(
                              color: context.colors.textPrimary,
                              fontSize: 16,
                              fontWeight: FontWeight.w400,
                            ),
                            decoration: InputDecoration(
                              hintText: l10n.phoneLoginCodeHint,
                              hintStyle: TextStyle(
                                color: context.colors.textTertiary,
                                fontSize: 16,
                              ),
                              counterText: '',
                              filled: true,
                              fillColor: Color(0xFFF3F3F3),
                              contentPadding: const EdgeInsets.symmetric(
                                horizontal: 20,
                                vertical: 16,
                              ),
                              border: OutlineInputBorder(
                                borderRadius: BorderRadius.circular(27),
                                borderSide: BorderSide.none,
                              ),
                              enabledBorder: OutlineInputBorder(
                                borderRadius: BorderRadius.circular(27),
                                borderSide: BorderSide.none,
                              ),
                              focusedBorder: OutlineInputBorder(
                                borderRadius: BorderRadius.circular(27),
                                borderSide: BorderSide(
                                  color: context.colors.primary,
                                  width: 1,
                                ),
                              ),
                              suffixIcon: Padding(
                                padding: const EdgeInsets.only(right: 12),
                                child: TextButton(
                                  onPressed: controller.canRequestCode
                                      ? controller.requestVerifyCode
                                      : null,
                                  style: TextButton.styleFrom(
                                    minimumSize: Size.zero,
                                    padding: const EdgeInsets.symmetric(
                                        horizontal: 8, vertical: 4),
                                    tapTargetSize:
                                        MaterialTapTargetSize.shrinkWrap,
                                    foregroundColor: context.colors.primary,
                                    disabledForegroundColor:
                                        context.colors.textTertiary,
                                    textStyle: const TextStyle(
                                      fontSize: 14,
                                      fontWeight: FontWeight.w500,
                                    ),
                                  ),
                                  child: Text(btnText),
                                ),
                              ),
                              suffixIconConstraints:
                                  const BoxConstraints(minWidth: 0),
                            ),
                          );
                        }),

                        const SizedBox(height: 56),

                        // 提示文字
                        Text(
                          l10n.phoneLoginAutoRegisterHint,
                          style: TextStyle(
                            color: context.colors.textTertiary,
                            fontSize: 12,
                            fontWeight: FontWeight.w400,
                          ),
                          textAlign: TextAlign.center,
                        ),

                        const SizedBox(height: 16),

                        // 立即登录按钮
                        Obx(() {
                          final isLoggingIn = controller.isLoggingIn.value;
                          return SizedBox(
                            height: 48,
                            child: ElevatedButton(
                              onPressed:
                                  controller.canLogin ? controller.login : null,
                              style: ElevatedButton.styleFrom(
                                backgroundColor: context.colors.primary,
                                foregroundColor: Colors.white,
                                disabledBackgroundColor: context.colors.primary
                                    .withValues(alpha: 0.4),
                                disabledForegroundColor: Colors.white,
                                elevation: 0,
                                shape: RoundedRectangleBorder(
                                  borderRadius: BorderRadius.circular(24),
                                ),
                                textStyle: const TextStyle(
                                  fontSize: 16,
                                  fontWeight: FontWeight.w600,
                                ),
                              ),
                              child: isLoggingIn
                                  ? Row(
                                      mainAxisAlignment:
                                          MainAxisAlignment.center,
                                      children: [
                                        const _SpinningLoader(),
                                        const SizedBox(width: 8),
                                        Text(l10n.phoneLoginLoggingIn),
                                      ],
                                    )
                                  : Text(l10n.loginBtn),
                            ),
                          );
                        }),
                      ],
                    ),
                  ),
                ],
              ),
            ),
            Positioned(
              top: MediaQuery.paddingOf(context).top,
              left: 0,
              child: IconButton(
                  highlightColor: Colors.transparent,
                  splashColor: Colors.transparent,
                  padding: EdgeInsets.zero,
                  onPressed: Get.back,
                  icon: Image.asset(
                    'assets/images/common/ic_nav_back.webp',
                    width: 24,
                    height: 24,
                  )),
            ),
          ],
        ),
      ),
    );
  }
}

// ── 旋转 Loading 图标 ────────────────────────────────────────────
class _SpinningLoader extends StatefulWidget {
  const _SpinningLoader();

  @override
  State<_SpinningLoader> createState() => _SpinningLoaderState();
}

class _SpinningLoaderState extends State<_SpinningLoader>
    with SingleTickerProviderStateMixin {
  late final AnimationController _ctrl;

  @override
  void initState() {
    super.initState();
    _ctrl = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 900),
    )..repeat();
  }

  @override
  void dispose() {
    _ctrl.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return RotationTransition(
      turns: _ctrl,
      child: Image.asset(
        'assets/images/common/ic_loading_ios_style.png',
        width: 24,
        height: 24,
      ),
    );
  }
}

// ── 手机号格式化 ───────────────────────────────────────────────────
class _PhoneTextInputFormatter extends TextInputFormatter {
  @override
  TextEditingValue formatEditUpdate(
    TextEditingValue oldValue,
    TextEditingValue newValue,
  ) {
    final text = newValue.text.replaceAll(' ', '');
    if (text.isEmpty) {
      return newValue;
    }

    final buffer = StringBuffer();
    for (int i = 0; i < text.length; i++) {
      buffer.write(text[i]);
      final nonLeadingIndex = i + 1;
      if (nonLeadingIndex == 3 || nonLeadingIndex == 7) {
        if (nonLeadingIndex < text.length) {
          buffer.write(' ');
        }
      }
    }

    final string = buffer.toString();
    return newValue.copyWith(
      text: string,
      selection: TextSelection.collapsed(offset: string.length),
    );
  }
}