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,345 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../../app/tab_index.dart';
import '../../../core/theme/app_colors.dart';
import '../../../shared/widgets/app_feedback.dart';
import '../../../shared/widgets/x_card.dart';
import '../../../shared/widgets/x_chip.dart';
import '../../../shared/widgets/x_common.dart';
import '../../../shared/widgets/x_icon.dart';
import '../../../shared/widgets/x_top_bar.dart';
import '../../auth/application/session_controller.dart';
import '../../auth/data/auth_models.dart';
import '../../customer/data/customer_api.dart';
import '../../dashboard/data/dashboard_api.dart';
/// 「我的」功能入口配置。
typedef _MenuItem = ({
String label,
String icon,
Color background,
Color color,
VoidCallback onTap,
});
/// 我的 Tab,对应 `components/tabs/MineTab.vue`。
class MineTab extends ConsumerStatefulWidget {
const MineTab({super.key});
@override
ConsumerState<MineTab> createState() => _MineTabState();
}
class _MineTabState extends ConsumerState<MineTab> {
int _customerCount = 0;
int _activeProjects = 0;
int _surveys = 0;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) => _load());
}
Future<void> _load() async {
var customerCount = _customerCount;
var activeProjects = _activeProjects;
var surveys = _surveys;
// 客户总数:独立接口
try {
final res = await ref
.read(customerApiProvider)
.list(page: 1, pageSize: 1);
if (res.isOk) customerCount = res.data?.total ?? 0;
} catch (_) {
// 静默刷新:请求失败时保留已渲染的数据
}
// 进行中项目 + 勘测任务:一次 dashboard 请求
try {
final res = await ref.read(dashboardApiProvider).stats();
final dash = res.isOk ? res.data : null;
if (dash != null) {
activeProjects = dash.contractTotal;
surveys = dash.surveyTotal;
}
} catch (_) {
// 静默刷新:请求失败时保留已渲染的数据
}
if (!mounted) return;
setState(() {
_customerCount = customerCount;
_activeProjects = activeProjects;
_surveys = surveys;
});
}
Future<void> _goSurvey() async {
await context.push('/survey');
if (mounted) _load();
}
Future<void> _onLogout() async {
final confirmed = await showAppConfirm(
title: '退出登录',
content: '确定要退出当前账号吗?',
destructive: true,
);
if (!confirmed) return;
await ref.read(sessionProvider.notifier).logout();
}
@override
Widget build(BuildContext context) {
ref.listen(tabIndexProvider, (previous, next) {
if (next == AppTab.mine) _load();
});
final user = ref.watch(sessionProvider).user;
final displayName = user?.displayName ?? '陈伟';
final avatarText = user?.avatarText ?? '陈';
final roleText = user == null ? '高级销售工程师' : roleLabel(user.roles);
final menus = <_MenuItem>[
(
label: '客户管理',
icon: 'groups',
background: const Color(0x140054CD),
color: AppColors.primary,
onTap: () =>
ref.read(tabIndexProvider.notifier).select(AppTab.customer),
),
(
label: '项目中心',
icon: 'calendar',
background: const Color(0x14712AE2),
color: AppColors.secondary,
onTap: () =>
ref.read(tabIndexProvider.notifier).select(AppTab.project),
),
(
label: '勘测任务',
icon: 'camera',
background: const Color(0x1400685F),
color: AppColors.tertiary,
onTap: _goSurvey,
),
(
label: '设置',
icon: 'more',
background: const Color(0x14E0852B),
color: AppColors.statusWarn,
onTap: () => showAppToast('设置功能即将上线'),
),
];
return XPageShell(
topBar: XTopBar.brand(title: '我的'),
child: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 130),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildUserCard(avatarText, displayName, roleText),
_buildStats(),
XCard(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: Column(
children: [
for (var i = 0; i < menus.length; i++) ...[
if (i > 0)
const Divider(
height: 1,
thickness: 1,
color: AppColors.borderRule,
),
_menuItem(menus[i]),
],
],
),
),
XPrimaryButton(
label: '退出登录',
onPressed: _onLogout,
height: 46,
margin: const EdgeInsets.only(top: 28, bottom: 40),
background: AppColors.surface,
foreground: AppColors.statusError,
borderColor: AppColors.statusError,
fontSize: 15,
pill: false,
),
],
),
),
);
}
Widget _buildUserCard(String avatarText, String displayName, String roleText) {
return XCard(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 18),
margin: const EdgeInsets.only(bottom: 14),
child: Row(
children: [
Container(
width: 60,
height: 60,
alignment: Alignment.center,
decoration: BoxDecoration(
color: AppColors.surfaceContainerHigh,
shape: BoxShape.circle,
border: Border.all(color: AppColors.primaryFixedDim, width: 2),
),
child: Text(
avatarText,
style: const TextStyle(
fontSize: 26,
fontWeight: FontWeight.w700,
color: AppColors.primary,
),
),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
displayName,
style: const TextStyle(
fontSize: 19,
fontWeight: FontWeight.w700,
color: AppColors.onSurface,
),
),
Padding(
padding: const EdgeInsets.only(top: 2),
child: Text(
roleText,
style: const TextStyle(
fontSize: 13,
color: AppColors.textMuted,
),
),
),
const Padding(
padding: EdgeInsets.only(top: 8),
child: XChip(
'在线',
tone: ChipTone.tertiary,
leading: _OnlineDot(),
),
),
],
),
),
const XIcon('forward', size: 20, color: AppColors.onSurfaceVariant),
],
),
);
}
Widget _buildStats() {
return XCard(
padding: const EdgeInsets.symmetric(vertical: 20),
margin: const EdgeInsets.only(bottom: 14),
child: Row(
children: [
_statCell('$_customerCount', '我的客户'),
const _StatDivider(),
_statCell('$_activeProjects', '进行中项目'),
const _StatDivider(),
_statCell('$_surveys', '勘测任务'),
],
),
);
}
Widget _statCell(String value, String label) {
return Expanded(
child: Column(
children: [
Text(
value,
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.w700,
color: AppColors.onSurface,
),
),
const SizedBox(height: 4),
Text(
label,
style: const TextStyle(fontSize: 12, color: AppColors.textMuted),
),
],
),
);
}
Widget _menuItem(_MenuItem menu) {
return GestureDetector(
onTap: menu.onTap,
behavior: HitTestBehavior.opaque,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 15),
child: Row(
children: [
Container(
width: 36,
height: 36,
alignment: Alignment.center,
decoration: BoxDecoration(
color: menu.background,
borderRadius: BorderRadius.circular(10),
),
child: XIcon(menu.icon, size: 20, color: menu.color),
),
const SizedBox(width: 14),
Expanded(
child: Text(
menu.label,
style: const TextStyle(
fontSize: 15,
color: AppColors.onSurface,
),
),
),
const XIcon('forward', size: 18, color: AppColors.outline),
],
),
),
);
}
}
/// 个人信息卡「在线」chip 左侧的状态圆点。
class _OnlineDot extends StatelessWidget {
const _OnlineDot();
@override
Widget build(BuildContext context) {
return Container(
width: 7,
height: 7,
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: AppColors.tertiary,
),
);
}
}
/// 统计卡内的 1px 分隔线。
class _StatDivider extends StatelessWidget {
const _StatDivider();
@override
Widget build(BuildContext context) {
return Container(width: 1, height: 32, color: AppColors.borderRule);
}
}