refactor: 移除前端 Mock API,新增共享组件,优化认证流程
- 删除 mock_api_client、mock_calendar_service、mock_history_service - 新增 fixed_length_code_input、link_button、message_composer 共享组件 - 优化登录/注册/密码重置页面使用新组件 - 简化 injection.dart 移除 mock 分支 - 更新 env.dart 配置(BACKEND_URL 替换 API_URL) - 后端 agentscope 工具和测试更新 - 重构 AGENTS.md 文档结构 - 新增 deploy/ 目录和 protocol 文档
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import '../../core/theme/design_tokens.dart';
|
||||
|
||||
typedef CodeValueChanged = void Function(String value);
|
||||
|
||||
class FixedLengthCodeInput extends StatefulWidget {
|
||||
final TextEditingController controller;
|
||||
final int length;
|
||||
final CodeValueChanged? onChanged;
|
||||
final TextInputType keyboardType;
|
||||
final Iterable<String>? allowedCharacters;
|
||||
final bool uppercase;
|
||||
final String semanticLabel;
|
||||
|
||||
const FixedLengthCodeInput({
|
||||
required this.controller,
|
||||
required this.length,
|
||||
required this.semanticLabel,
|
||||
super.key,
|
||||
this.onChanged,
|
||||
this.keyboardType = TextInputType.text,
|
||||
this.allowedCharacters,
|
||||
this.uppercase = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State<FixedLengthCodeInput> createState() => _FixedLengthCodeInputState();
|
||||
}
|
||||
|
||||
class _FixedLengthCodeInputState extends State<FixedLengthCodeInput> {
|
||||
late final FocusNode _focusNode;
|
||||
bool _isFocused = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_focusNode = FocusNode();
|
||||
_focusNode.addListener(_onFocusChanged);
|
||||
widget.controller.addListener(_onControllerChanged);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant FixedLengthCodeInput oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.controller != widget.controller) {
|
||||
oldWidget.controller.removeListener(_onControllerChanged);
|
||||
widget.controller.addListener(_onControllerChanged);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.controller.removeListener(_onControllerChanged);
|
||||
_focusNode.removeListener(_onFocusChanged);
|
||||
_focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onFocusChanged() {
|
||||
if (_isFocused != _focusNode.hasFocus) {
|
||||
_isFocused = _focusNode.hasFocus;
|
||||
}
|
||||
}
|
||||
|
||||
void _onControllerChanged() {
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
void _handleRawChanged(String rawValue) {
|
||||
final normalized = _normalize(rawValue);
|
||||
if (normalized != widget.controller.text) {
|
||||
widget.controller.value = TextEditingValue(
|
||||
text: normalized,
|
||||
selection: TextSelection.collapsed(offset: normalized.length),
|
||||
);
|
||||
}
|
||||
widget.onChanged?.call(normalized);
|
||||
}
|
||||
|
||||
String _normalize(String value) {
|
||||
var output = widget.uppercase ? value.toUpperCase() : value;
|
||||
|
||||
if (widget.allowedCharacters != null) {
|
||||
final allow = widget.allowedCharacters!.toSet();
|
||||
output = output.split('').where(allow.contains).join();
|
||||
}
|
||||
|
||||
if (output.length > widget.length) {
|
||||
output = output.substring(0, widget.length);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final chars = widget.controller.text.split('');
|
||||
final slotHeight = AppSpacing.xl * 2;
|
||||
final slotSpacing = AppSpacing.sm;
|
||||
|
||||
return Semantics(
|
||||
label: widget.semanticLabel,
|
||||
child: GestureDetector(
|
||||
onTap: () => _focusNode.requestFocus(),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: SizedBox(
|
||||
height: slotHeight,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Opacity(
|
||||
opacity: 0,
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
height: slotHeight,
|
||||
child: TextField(
|
||||
controller: widget.controller,
|
||||
focusNode: _focusNode,
|
||||
keyboardType: widget.keyboardType,
|
||||
inputFormatters: [
|
||||
LengthLimitingTextInputFormatter(widget.length),
|
||||
],
|
||||
onChanged: _handleRawChanged,
|
||||
autofillHints: const [AutofillHints.oneTimeCode],
|
||||
),
|
||||
),
|
||||
),
|
||||
IgnorePointer(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
for (var index = 0; index < widget.length; index++) ...[
|
||||
Expanded(
|
||||
child: _buildCodeCell(
|
||||
index: index,
|
||||
chars: chars,
|
||||
slotHeight: slotHeight,
|
||||
),
|
||||
),
|
||||
if (index != widget.length - 1)
|
||||
SizedBox(width: slotSpacing),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCodeCell({
|
||||
required int index,
|
||||
required List<String> chars,
|
||||
required double slotHeight,
|
||||
}) {
|
||||
final hasChar = index < chars.length;
|
||||
final isActive =
|
||||
(chars.length == index && _isFocused) ||
|
||||
(chars.length >= widget.length && index == widget.length - 1);
|
||||
|
||||
return Container(
|
||||
height: slotHeight,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.white,
|
||||
borderRadius: BorderRadius.circular(AppRadius.sm),
|
||||
border: Border.all(
|
||||
color: isActive ? AppColors.primary : AppColors.slate300,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
hasChar ? chars[index] : '',
|
||||
style: const TextStyle(
|
||||
fontSize: AppSpacing.xl,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.slate900,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user