AI 接入 V1.0

UI更新
This commit is contained in:
2026-09-24 14:24:34 +08:00
parent 34bc2e04b0
commit 9a144f159f
65 changed files with 1195 additions and 196 deletions
+44 -143
View File
@@ -10,8 +10,13 @@ import '../../providers/auth_provider.dart';
import '../../utils/crypto.dart';
import '../../utils/toast.dart';
import '../shell/main_shell.dart';
import '../../widgets/cute_icon.dart';
/// 登录/注册页(复刻 uniApp pages/login/login.vue)
/// 登录页(复刻 uniApp pages/login/login.vue)
///
/// **注册功能已下线**(2026-09-24):这是单人应用,开放注册等于任何人都能建号。
/// App 端的入口和界面都去掉了;后端 `/api/auth/register` 按用户要求保留着,
/// 真要加人时把入口加回来即可。
class LoginPage extends ConsumerStatefulWidget {
const LoginPage({super.key});
@@ -21,15 +26,12 @@ class LoginPage extends ConsumerStatefulWidget {
class _LoginPageState extends ConsumerState<LoginPage>
with SingleTickerProviderStateMixin {
bool _isRegister = false;
bool _showPassword = false;
bool _loading = false;
bool _agreeProtocol = false;
bool _rememberMe = false;
final _usernameController = TextEditingController();
final _passwordController = TextEditingController();
final _nicknameController = TextEditingController();
final _formKey = GlobalKey<FormState>();
late final AnimationController _floatController = AnimationController(
@@ -53,7 +55,6 @@ class _LoginPageState extends ConsumerState<LoginPage>
_floatController.dispose();
_usernameController.dispose();
_passwordController.dispose();
_nicknameController.dispose();
super.dispose();
}
@@ -66,43 +67,24 @@ class _LoginPageState extends ConsumerState<LoginPage>
showToast(context, '请输入用户名和密码', error: true);
return;
}
if (_isRegister) {
if (password.length < 6) {
showToast(context, '密码至少6位', error: true);
return;
}
if (!_agreeProtocol) {
showToast(context, '请先阅读并同意用户协议');
return;
}
}
setState(() => _loading = true);
try {
final api = ref.read(authApiProvider);
final response = _isRegister
? await api.register(
username,
password,
_nicknameController.text.trim().isEmpty
? username
: _nicknameController.text.trim(),
)
: await api.login(username, password);
final response = await ref.read(authApiProvider).login(username, password);
// 记住密码
if (!_isRegister && _rememberMe) {
if (_rememberMe) {
await LocalStorage.instance.saveRememberedPassword(
username,
encryptPassword(password),
);
} else if (!_isRegister && !_rememberMe) {
} else {
await LocalStorage.instance.clearRememberedPassword();
}
await ref.read(authProvider.notifier).loginSuccess(response);
if (!mounted) return;
showToast(context, _isRegister ? '🎉 注册成功' : '🚀 登录成功');
showToast(context, '登录成功', icon: 'check');
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (_) => const MainShell()),
(route) => false,
@@ -308,90 +290,55 @@ class _LoginPageState extends ConsumerState<LoginPage>
key: _formKey,
child: Column(
children: [
_buildSegmentTabs(),
const SizedBox(height: 20),
_inputField(
icon: '👤',
icon: 'user',
controller: _usernameController,
hint: '请输入用户名',
maxLength: 20,
),
const SizedBox(height: 14),
_inputField(
icon: '🔒',
icon: 'lock',
controller: _passwordController,
hint: '请输入密码',
maxLength: 20,
obscure: !_showPassword,
suffix: IconButton(
icon: Text(
_showPassword ? '👁️' : '👁️‍🗨️',
style: const TextStyle(fontSize: 16),
// 一个图标用颜色表示状态:显示中=品牌色,隐藏中=浅灰
icon: CuteIcon(
'eye',
size: 18,
tint: _showPassword
? const Color(0xFFC8956E)
: const Color(0xFFBCAAA4),
),
onPressed: () =>
setState(() => _showPassword = !_showPassword),
),
),
if (_isRegister) ...[
const SizedBox(height: 14),
_inputField(
icon: '✨',
controller: _nicknameController,
hint: '请输入昵称(可选)',
maxLength: 20,
),
],
if (!_isRegister) ...[
const SizedBox(height: 8),
Row(
children: [
SizedBox(
width: 22,
height: 22,
child: Checkbox(
value: _rememberMe,
activeColor: const Color(0xFFC8956E),
onChanged: (v) =>
setState(() => _rememberMe = v ?? false),
),
const SizedBox(height: 8),
Row(
children: [
SizedBox(
width: 22,
height: 22,
child: Checkbox(
value: _rememberMe,
activeColor: const Color(0xFFC8956E),
onChanged: (v) =>
setState(() => _rememberMe = v ?? false),
),
const SizedBox(width: 6),
const Text(
'记住密码',
style: TextStyle(
fontSize: 13,
color: Color(0xFF8D6E63),
),
),
const SizedBox(width: 6),
const Text(
'记住密码',
style: TextStyle(
fontSize: 13,
color: Color(0xFF8D6E63),
),
],
),
],
if (_isRegister) ...[
const SizedBox(height: 8),
Row(
children: [
SizedBox(
width: 22,
height: 22,
child: Checkbox(
value: _agreeProtocol,
activeColor: const Color(0xFFC8956E),
onChanged: (v) =>
setState(() => _agreeProtocol = v ?? false),
),
),
const Expanded(
child: Text(
'我已阅读并同意用户协议',
style: TextStyle(
fontSize: 13,
color: Color(0xFF8D6E63),
),
),
),
],
),
],
),
],
),
const SizedBox(height: 24),
SizedBox(
width: double.infinity,
@@ -414,9 +361,9 @@ class _LoginPageState extends ConsumerState<LoginPage>
color: Colors.white,
),
)
: Text(
_isRegister ? '注 册' : '登 录',
style: const TextStyle(
: const Text(
'登 录',
style: TextStyle(
fontSize: 17,
fontWeight: FontWeight.bold,
letterSpacing: 4,
@@ -437,52 +384,6 @@ class _LoginPageState extends ConsumerState<LoginPage>
sigmaY: 10,
);
// ---- 登录/注册切换 ----
Widget _buildSegmentTabs() {
return Container(
decoration: BoxDecoration(
color: const Color(0x33C8956E),
borderRadius: BorderRadius.circular(20),
),
padding: const EdgeInsets.all(4),
child: Row(
children: [
_segment(
'登录',
!_isRegister,
() => setState(() => _isRegister = false),
),
_segment('注册', _isRegister, () => setState(() => _isRegister = true)),
],
),
);
}
Widget _segment(String text, bool active, VoidCallback onTap) {
return Expanded(
child: GestureDetector(
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(vertical: 10),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
color: active ? const Color(0xFFC8956E) : Colors.transparent,
),
child: Center(
child: Text(
text,
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: active ? Colors.white : const Color(0xFF8D6E63),
),
),
),
),
),
);
}
Widget _inputField({
required String icon,
@@ -501,7 +402,7 @@ class _LoginPageState extends ConsumerState<LoginPage>
),
child: Row(
children: [
Text(icon, style: const TextStyle(fontSize: 18)),
CuteIcon(icon, size: 20),
const SizedBox(width: 10),
Expanded(
child: TextField(