hi-client/lib/app/modules/kr_login/views/kr_login_view.dart

468 lines
16 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(
textController: 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 textController,
required String hintText,
bool isPassword = false,
List<String>? suffixes,
List<String>? historyEmails,
}) {
// 基础输入框构建逻辑提取
Widget buildTextField({
FocusNode? focusNode,
VoidCallback? onEditingComplete,
}) {
return TextField(
controller: textController,
focusNode: focusNode,
contextMenuBuilder: (context, editableTextState) => const SizedBox.shrink(),
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: textController,
focusNode: this.controller.kr_accountFocusNode,
optionsBuilder: (TextEditingValue textEditingValue) {
final inputText = textEditingValue.text;
// 结果列表
List<String> options = [];
// 1. 匹配历史记录 (只要输入内容匹配历史记录的开头,或者输入为空)
if (historyEmails != null) {
if (inputText.isEmpty) {
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, // This textEditingController is provided by RawAutocomplete
FocusNode focusNode,
VoidCallback onFieldSubmitted,
) {
// Here we use the passed in textController, not the textEditingController provided by fieldViewBuilder
// because we need to control the textController externally.
// Note: RawAutocomplete listens to textEditingController by default,
// if we pass our own textController to RawAutocomplete,
// the fieldViewBuilder's textEditingController is actually the one we passed.
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,
contextMenuBuilder: (context, editableTextState) => const SizedBox.shrink(),
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();
}
}
}