feat: 完成 Flutter 版智能电网 IoT 管理应用初始化搭建

This commit is contained in:
andyesfly committed 2026-10-02 17:21:53 +08:00
commit f61b7bf90b
120 files changed
+15229

No files matched your search

@@ -0,0 +1,279 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../../core/theme/app_colors.dart';
import '../../../core/theme/app_dimens.dart';
import '../../../core/theme/app_typography.dart';
import '../../../shared/widgets/x_card.dart';
import '../../../shared/widgets/x_common.dart';
import '../../../shared/widgets/x_icon.dart';
import '../application/session_controller.dart';
/// 登录页,对应 `pages/login/index.vue`。
class LoginPage extends ConsumerStatefulWidget {
const LoginPage({super.key});
@override
ConsumerState<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends ConsumerState<LoginPage> {
final _username = TextEditingController();
final _password = TextEditingController();
bool _loading = false;
String _errorMsg = '';
@override
void dispose() {
_username.dispose();
_password.dispose();
super.dispose();
}
Future<void> _handleLogin() async {
final username = _username.text.trim();
final password = _password.text;
if (username.isEmpty || password.isEmpty) {
setState(() => _errorMsg = '请输入账号和密码');
return;
}
setState(() {
_loading = true;
_errorMsg = '';
});
try {
final ok = await ref
.read(sessionProvider.notifier)
.login(username: username, password: password);
if (!mounted) return;
if (ok) {
context.go('/tabs');
} else {
setState(() => _errorMsg = '登录失败,请检查账号密码');
}
} catch (e) {
if (!mounted) return;
setState(() => _errorMsg = e.toString());
} finally {
if (mounted) setState(() => _loading = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.bgPage,
body: Stack(
children: [
// 源页面右上/左下的两处柔光渐变
const Positioned(
right: -80,
top: -80,
child: _SoftBlob(color: Color(0x0F0054CD), size: 280),
),
const Positioned(
left: -80,
bottom: -80,
child: _SoftBlob(color: Color(0x0D712AE2), size: 280),
),
LayoutBuilder(
builder: (context, constraints) => SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 40),
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: (constraints.maxHeight - 80).clamp(0, double.infinity),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildBrand(),
const SizedBox(height: 40),
_buildForm(),
const SizedBox(height: 40),
Text(
'© 鑫潼智电 · 运营管理',
style: const TextStyle(
fontSize: 12,
color: AppColors.textMuted,
),
),
],
),
),
),
),
],
),
);
}
Widget _buildBrand() {
return Column(
children: [
Container(
width: 64,
height: 64,
decoration: BoxDecoration(
gradient: const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [AppColors.brandGradientStart, AppColors.brandGradientEnd],
),
borderRadius: BorderRadius.circular(20),
boxShadow: const [
BoxShadow(
color: Color(0x401D6FFF),
blurRadius: 20,
offset: Offset(0, 8),
),
],
),
alignment: Alignment.center,
child: const XIcon('bolt', size: 34, color: Colors.white),
),
const SizedBox(height: 16),
Text(
'鑫潼智电',
style: AppTypography.headlineMd.copyWith(
fontSize: 26,
letterSpacing: 2,
),
),
const SizedBox(height: 4),
const Text(
'鑫潼智电运营管理平台',
style: TextStyle(fontSize: 14, color: AppColors.textMuted),
),
],
);
}
Widget _buildForm() {
return XCard(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildField(
label: '账号',
icon: 'person',
controller: _username,
hintText: '请输入账号',
obscure: false,
),
const SizedBox(height: 18),
_buildField(
label: '密码',
icon: 'more',
controller: _password,
hintText: '请输入密码',
obscure: true,
),
const SizedBox(height: 8),
XPrimaryButton(
label: '登 录',
loading: _loading,
onPressed: _handleLogin,
),
if (_errorMsg.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 14),
child: Text(
_errorMsg,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 13,
color: AppColors.statusError,
),
),
),
],
),
);
}
Widget _buildField({
required String label,
required String icon,
required TextEditingController controller,
required String hintText,
required bool obscure,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: AppTypography.labelBold.copyWith(
color: AppColors.onSurfaceVariant,
),
),
const SizedBox(height: 8),
Container(
height: 48,
padding: const EdgeInsets.symmetric(horizontal: 14),
decoration: BoxDecoration(
color: AppColors.surfaceBright,
border: Border.all(color: AppColors.borderRule),
borderRadius: BorderRadius.circular(AppRadius.lg),
),
child: Row(
children: [
XIcon(icon, size: 20, color: AppColors.textMuted),
const SizedBox(width: 10),
Expanded(
child: TextField(
controller: controller,
obscureText: obscure,
style: const TextStyle(
fontSize: 15,
color: AppColors.onSurface,
),
cursorColor: AppColors.primary,
decoration: InputDecoration(
isDense: true,
border: InputBorder.none,
hintText: hintText,
hintStyle: const TextStyle(
fontSize: 15,
color: AppColors.textPlaceholder,
),
),
),
),
],
),
),
],
);
}
}
/// 登录页背景的柔光圆斑(对应源页面两处 radial-gradient)。
class _SoftBlob extends StatelessWidget {
const _SoftBlob({required this.color, required this.size});
final Color color;
final double size;
@override
Widget build(BuildContext context) {
return IgnorePointer(
child: Container(
width: size,
height: size,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: RadialGradient(
colors: [color, color.withValues(alpha: 0)],
),
),
),
);
}
}