feat: 完成 Flutter 版智能电网 IoT 管理应用初始化搭建
This commit is contained in:
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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user