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:
qzl
2026-03-12 16:41:45 +08:00
parent d7fbb74bf8
commit 01c36eb32e
70 changed files with 5138 additions and 5829 deletions
@@ -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,
),
),
);
}
}