import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:kaer_with_panels/app/utils/kr_log_util.dart'; import '../controllers/kr_login_controller.dart'; import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart'; import 'package:kaer_with_panels/app/localization/app_translations.dart'; import 'package:kaer_with_panels/app/widgets/kr_local_image.dart'; import 'package:kaer_with_panels/app/widgets/hi_base_scaffold.dart'; import 'package:kaer_with_panels/app/widgets/hi_help_entrance.dart'; import 'package:kaer_with_panels/app/common/app_run_data.dart'; import 'package:kaer_with_panels/app/modules/kr_home/controllers/kr_home_controller.dart'; import 'package:kaer_with_panels/app/services/kr_site_config_service.dart'; import 'package:flutter/foundation.dart'; import 'package:kaer_with_panels/app/widgets/kr_subscription_expiry_text.dart'; import 'package:flutter/services.dart'; class KRLoginView extends GetView { const KRLoginView({super.key}); @override Widget build(BuildContext context) { final isKeyboardVisible = MediaQuery.of(context).viewInsets.bottom > 0; final isHideBack = (Get.arguments as Map?)?['is-back'] ?? false; return HIBaseScaffold( showBack: !isHideBack, resizeToAvoidBottomInset: true, child: Stack( children: [ GestureDetector( behavior: HitTestBehavior.translucent, onTap: () { FocusScope.of(context).unfocus(); }, child: SingleChildScrollView( child: Padding( padding: EdgeInsets.symmetric( horizontal: 40.w, ), child: Column( children: [ // Text( // '${controller.kr_loginStatus.value}', // style: TextStyle(color: Colors.white), // 使用 TextStyle() // ), SizedBox(height: 20.w), _buildUserInfoSection(), SizedBox(height: 12.w), _buildBindEmailLayout(), SizedBox(height: 100.w), // 为底部帮助按钮留出空间 ], ), ), ), ), if (!isKeyboardVisible) const HIHelpEntrance(), ], ), ); } Widget _buildUserInfoSection() { return Row( children: [ Container( width: 60.w, height: 60.w, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(18.r), ), alignment: Alignment.center, child: KrLocalImage( imageName: 'hi-home-logo', imageType: ImageType.svg, width: 30.w, height: 30.w, color: Colors.black, ), ), SizedBox(width: 12.w), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ Obx(() { final account = KRAppRunData.getInstance().kr_account.value; final isDeviceLogin = account != null && account.startsWith('9000'); final accountText = (account ==null || isDeviceLogin) ? '待绑定' : '${KRAppRunData.getInstance().kr_account.value.toString()}'; return Text( accountText, style: TextStyle( color: Colors.white.withOpacity(0.85), fontSize: 14.sp, fontWeight: FontWeight.w500, ), ); }), KRSubscriptionExpiryText( expireTimeProvider: () => controller .kr_subscribeService.kr_currentSubscribe.value?.expireTime, style: TextStyle( color: Colors.white, fontSize: 12.sp, ), ), ], ), ), ], ); } Widget _buildBindEmailLayout() { return Column( children: [ Container( constraints: BoxConstraints(minHeight: 300.w), child: Column( children: [ Obx(() => _buildStandardInputField( controller: controller.accountController, hintText: '请输入邮箱地址', suffixes: const [ '@gmail.com', '@outlook.com', '@qq.com', '@163.com' ], historyEmails: controller.kr_emailHistory.toList(), )), // SizedBox(height: 10.w), // _buildStandardInputField( // controller: controller.psdController, // hintText: '新密码', // isPassword: true, // ), // SizedBox(height: 10.w), // _buildStandardInputField( // controller: controller.agPsdController, // hintText: '确认密码', // isPassword: true, // ), SizedBox(height: 10), // 👇 核心改动:使用新的验证码输入框 _buildVerificationCodeField(), ], ), ), SizedBox(height: 30.h), _buildSaveButton(), ], ); } /// 构建标准输入框 Widget _buildStandardInputField({ required TextEditingController controller, required String hintText, bool isPassword = false, List? suffixes, List? historyEmails, }) { // 基础输入框构建逻辑提取 Widget buildTextField({ FocusNode? focusNode, VoidCallback? onEditingComplete, }) { return TextField( controller: controller, focusNode: focusNode, onEditingComplete: onEditingComplete, obscureText: isPassword, style: KrAppTextStyle( fontSize: 16, color: Colors.white, ), decoration: InputDecoration( hintText: hintText, hintStyle: KrAppTextStyle( fontSize: 16, color: const Color(0xFFA6A6A6), fontWeight: FontWeight.w600, ), contentPadding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 10.w), border: OutlineInputBorder( borderRadius: BorderRadius.circular(25.w), // 调整为更圆的角 borderSide: BorderSide(color: Colors.white, width: 2), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(25.w), borderSide: BorderSide(color: Colors.white, width: 2), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(25.w), borderSide: BorderSide(color: Colors.white, width: 2), ), ), ); } // 如果即没有后缀配置也没有历史记录,直接返回普通输入框 if ((suffixes == null || suffixes.isEmpty) && (historyEmails == null || historyEmails.isEmpty)) { return SizedBox( child: buildTextField(), ); } // 使用 RawAutocomplete 实现带提示的输入框 return SizedBox( child: RawAutocomplete( textEditingController: controller, focusNode: FocusNode(), optionsBuilder: (TextEditingValue textEditingValue) { final inputText = textEditingValue.text; // 结果列表 List options = []; // 1. 匹配历史记录 (只要输入内容匹配历史记录的开头,或者输入为空) if (historyEmails != null) { if (inputText.isEmpty) { // 理论上 RawAutocomplete 默认不显示空输入的 options,除非自定义 fieldViewBuilder 监听 // 但 RawAutocomplete 的 optionsBuilder 在 text 变化时触发。 // 若要空内容显示,通常需要 Focus 触发。这里先处理有内容的情况, // 或者如果 RawAutocomplete 支持空内容(通过 initialValue? 不行,得看 triggerMode) // 简单处理:如果为空,返回所有历史记录 options.addAll(historyEmails); } else { options.addAll(historyEmails .where((email) => email.startsWith(inputText))); } } // 2. 匹配后缀 (仅当有输入且不含 @ 或含 @ 但未完整时) if (suffixes != null && inputText.isNotEmpty) { if (!inputText.contains('@')) { options.addAll(suffixes.map((suffix) => '$inputText$suffix')); } else { final atIndex = inputText.indexOf('@'); final prefix = inputText.substring(0, atIndex); final domainInput = inputText.substring(atIndex); // 包含 @ options.addAll(suffixes .where((suffix) => suffix.startsWith(domainInput)) .map((suffix) => '$prefix$suffix')); } } // 去重 return options.toSet().toList(); }, fieldViewBuilder: ( BuildContext context, TextEditingController textEditingController, FocusNode focusNode, VoidCallback onFieldSubmitted, ) { // 这里我们使用传入的 controller,而不是 fieldViewBuilder 提供的 textEditingController // 因为我们需要外部控制 controller。 // 注意:RawAutocomplete 默认会监听 textEditingController, // 如果我们传入了自己的 controller 给 RawAutocomplete, // fieldViewBuilder 的 textEditingController 其实就是我们传入的那个。 return buildTextField( focusNode: focusNode, onEditingComplete: onFieldSubmitted, ); }, optionsViewBuilder: ( BuildContext context, AutocompleteOnSelected onSelected, Iterable options, ) { return Align( alignment: Alignment.topLeft, child: Material( elevation: 4.0, color: const Color(0xFF1E1E1E), // 深色背景 borderRadius: BorderRadius.circular(8.r), child: Container( constraints: BoxConstraints( maxHeight: 200.w, maxWidth: 300.w, //略小于屏幕宽度,或者根据父级宽度动态计算更好,这里简单给定 ), width: MediaQuery.of(context).size.width - 80.w, // 减去两边 padding child: ListView.builder( padding: EdgeInsets.zero, shrinkWrap: true, itemCount: options.length, itemBuilder: (BuildContext context, int index) { final String option = options.elementAt(index); return InkWell( onTap: () { onSelected(option); }, child: Padding( padding: EdgeInsets.symmetric( horizontal: 16.w, vertical: 12.w), child: Text( option, style: KrAppTextStyle( fontSize: 14, color: Colors.white, ), ), ), ); }, ), ), ), ); }, ), ); } /// 构建注册页面的验证码输入框(包含间距) Widget _buildVerificationCodeField() { // 从站点配置服务获取验证配置(站点配置不是响应式的,不需要 Obx) // final siteConfig = KRSiteConfigService(); // final needVerification = siteConfig.isEmailVerificationEnabled() || // siteConfig.isRegisterVerificationEnabled(); // // // 如果不需要验证码,返回空容器 // if (!needVerification) { // return SizedBox.shrink(); // } return SizedBox( height: 50, // 固定高度 child: TextField( controller: controller.codeController, keyboardType: TextInputType.number, textInputAction: TextInputAction.done, autofillHints: const [AutofillHints.oneTimeCode], enableSuggestions: false, autocorrect: false, inputFormatters: [FilteringTextInputFormatter.digitsOnly], onChanged: (value) { var v = value.replaceAll(RegExp("\\s+"), ""); const maxLen = 6; if (v.length > maxLen) { v = v.substring(0, maxLen); } if (controller.codeController.text != v) { controller.codeController.value = controller.codeController.value.copyWith( text: v, selection: TextSelection.collapsed(offset: v.length), composing: TextRange.empty, ); } if (v.isNotEmpty && (v.length >= 6)) { FocusScope.of(Get.context!).unfocus(); } }, onSubmitted: (_) { FocusScope.of(Get.context!).unfocus(); }, style: KrAppTextStyle( fontSize: 16, color: Colors.white, ), decoration: InputDecoration( hintText: '验证码', hintStyle: KrAppTextStyle( fontSize: 16, color: const Color(0xFFA6A6A6), fontWeight: FontWeight.w600, ), contentPadding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 10.w), border: OutlineInputBorder( borderRadius: BorderRadius.circular(25.w), borderSide: BorderSide(color: Colors.white, width: 2), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(25.w), borderSide: BorderSide(color: Colors.white, width: 2), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(25.w), borderSide: BorderSide(color: Colors.white, width: 2), ), isDense: true, suffixIconConstraints: BoxConstraints( maxHeight: 50.w, // 限制最大高度 ), suffixIcon: Padding( padding: EdgeInsets.symmetric(horizontal: 5.w, vertical: 5.w), child: Obx(() { return GestureDetector( onTap: controller.kr_canSendCode.value ? () => controller.kr_sendCode() : null, child: Container( width: 100.w, alignment: Alignment.center, decoration: BoxDecoration( color: controller.kr_canSendCode.value ? Theme.of(Get.context!).primaryColor : const Color(0xFFD5D5D5), borderRadius: BorderRadius.circular(100.r), // 药丸状 ), child: Text( controller.kr_countdownText.value, style: KrAppTextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: controller.kr_canSendCode.value ? Colors.black : const Color(0xFF464655), ), ), ), ); }), ), ), ), ); } Widget _buildSaveButton() { return GestureDetector( onTap: () { _handleSave(); }, child: Container( width: double.infinity, height: 50, decoration: BoxDecoration( color: Theme.of(Get.context!).primaryColor, borderRadius: BorderRadius.circular(100.r), ), child: Center( child: Text( controller.kr_getNextBtnText(), style: KrAppTextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: Colors.black, ), ), ), ), ); } void _handleSave() { final entry = (Get.arguments as Map?)?['entry']; if (entry == 'forget_psd') { controller.kr_setNewPsdByForgetPsd(); } else if (entry == 'bind_email') { controller.kr_register(); } else if (entry == 'login') { controller.kr_check(); } } }