hi-client/lib/app/modules/kr_login/views/kr_login_view.dart
2026-01-22 16:58:54 -08:00

470 lines
17 KiB
Dart
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<KRLoginController> {
const KRLoginView({super.key});
@override
Widget build(BuildContext context) {
final isKeyboardVisible = MediaQuery.of(context).viewInsets.bottom > 0;
final isHideBack =
(Get.arguments as Map<String, dynamic>?)?['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<String>? suffixes,
List<String>? 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<String>(
textEditingController: controller,
focusNode: FocusNode(),
optionsBuilder: (TextEditingValue textEditingValue) {
final inputText = textEditingValue.text;
// 结果列表
List<String> 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<String> onSelected,
Iterable<String> 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<String, dynamic>?)?['entry'];
if (entry == 'forget_psd') {
controller.kr_setNewPsdByForgetPsd();
} else if (entry == 'bind_email') {
controller.kr_register();
} else if (entry == 'login') {
controller.kr_check();
}
}
}