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

+37
View File
@@ -0,0 +1,37 @@
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../core/app_keys.dart';
import '../core/theme/app_theme.dart';
import '../core/theme/app_typography.dart';
import 'router.dart';
class SmartGridApp extends ConsumerWidget {
const SmartGridApp({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final router = ref.watch(routerProvider);
return MaterialApp.router(
title: '鑫潼智电运营管理',
debugShowCheckedModeBanner: false,
theme: AppTheme.build(),
routerConfig: router,
scaffoldMessengerKey: AppKeys.scaffoldMessenger,
locale: const Locale('zh', 'CN'),
supportedLocales: const [Locale('zh', 'CN'), Locale('en', 'US')],
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
// 对应 App.vue 中挂在 `page` 上的全局字体设置:15px / 行高 1.6 / on-surface。
builder: (context, child) => DefaultTextStyle(
style: appDefaultTextStyle,
child: child ?? const SizedBox.shrink(),
),
);
}
}
+109
View File
@@ -0,0 +1,109 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../core/app_keys.dart';
import '../features/auth/application/session_controller.dart';
import '../features/auth/presentation/login_page.dart';
import '../features/customer/presentation/customer_detail_page.dart';
import '../features/customer/presentation/customer_edit_page.dart';
import '../features/project/presentation/project_detail_page.dart';
import '../features/survey/presentation/survey_form_page.dart';
import '../features/survey/presentation/survey_list_page.dart';
import '../features/visit/presentation/visit_batch_page.dart';
import '../features/visit/presentation/visit_detail_page.dart';
import '../features/visit/presentation/visit_execute_page.dart';
import '../features/visit/presentation/visit_form_page.dart';
import '../features/visit/presentation/visit_list_page.dart';
import 'tabs_page.dart';
/// 会话语义变化时驱动 go_router 重新执行 `redirect`。
class _SessionRefresh extends ChangeNotifier {
void bump() => notifyListeners();
}
/// 路由表,对应 uni-app 版 `pages.json` 的 12 个页面。
///
/// 除登录页与 Tab 容器页外,其余均为二级页,统一使用主题里的
/// `CupertinoPageTransitionsBuilder`(新页面从右侧滑入),
/// 与源项目 `animationType: slide-in-right` 的表现一致。
final routerProvider = Provider<GoRouter>((ref) {
final refresh = _SessionRefresh();
ref.listen(sessionProvider, (_, _) => refresh.bump());
ref.onDispose(refresh.dispose);
int? queryId(GoRouterState state) =>
int.tryParse(state.uri.queryParameters['id'] ?? '');
return GoRouter(
navigatorKey: AppKeys.rootNavigator,
initialLocation: '/login',
refreshListenable: refresh,
redirect: (context, state) {
final loggedIn = ref.read(sessionProvider).isLoggedIn;
final atLogin = state.matchedLocation == '/login';
if (!loggedIn) return atLogin ? null : '/login';
if (atLogin) return '/tabs';
return null;
},
routes: [
GoRoute(path: '/login', builder: (context, state) => const LoginPage()),
GoRoute(path: '/tabs', builder: (context, state) => const TabsPage()),
// 客户模块
GoRoute(
path: '/customer/detail/:id',
builder: (context, state) =>
CustomerDetailPage(id: int.parse(state.pathParameters['id']!)),
),
GoRoute(
path: '/customer/edit',
builder: (context, state) => CustomerEditPage(id: queryId(state)),
),
// 拜访模块
GoRoute(
path: '/visit',
builder: (context, state) => const VisitListPage(),
),
GoRoute(
path: '/visit/detail/:id',
builder: (context, state) =>
VisitDetailPage(id: int.parse(state.pathParameters['id']!)),
),
GoRoute(
path: '/visit/form',
builder: (context, state) => VisitFormPage(id: queryId(state)),
),
GoRoute(
path: '/visit/execute/:id',
builder: (context, state) =>
VisitExecutePage(id: int.parse(state.pathParameters['id']!)),
),
GoRoute(
path: '/visit/batch',
builder: (context, state) => const VisitBatchPage(),
),
// 勘测模块
GoRoute(
path: '/survey',
builder: (context, state) => const SurveyListPage(),
),
GoRoute(
path: '/survey/form',
builder: (context, state) => SurveyFormPage(
id: queryId(state),
customerId: int.tryParse(state.uri.queryParameters['customerId'] ?? ''),
),
),
// 项目模块(按后端 project-plans 真实字段重做)
GoRoute(
path: '/project/detail/:id',
builder: (context, state) =>
ProjectDetailPage(id: int.parse(state.pathParameters['id']!)),
),
],
);
});
+31
View File
@@ -0,0 +1,31 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
/// 底部导航的四个 Tab,对应 `components/XBottomNav.vue` 的 4 个入口。
enum AppTab { workbench, customer, project, mine }
extension AppTabKey on AppTab {
/// 与 `XBottomNav.items` 中的 key 一致。
String get key => switch (this) {
AppTab.workbench => 'workbench',
AppTab.customer => 'customer',
AppTab.project => 'project',
AppTab.mine => 'mine',
};
}
/// 当前 Tab。对应 uni-app 版 `utils/tabs.ts` 的 `currentTab` ref + `switchTab`。
class TabIndexController extends Notifier<AppTab> {
@override
AppTab build() => AppTab.workbench;
void select(AppTab tab) => state = tab;
/// 按 `XBottomNav` 回传的字符串 key 切换。
void selectByKey(String key) {
state = AppTab.values.firstWhere((t) => t.key == key, orElse: () => state);
}
}
final tabIndexProvider = NotifierProvider<TabIndexController, AppTab>(
TabIndexController.new,
);
+52
View File
@@ -0,0 +1,52 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../core/theme/app_colors.dart';
import '../features/customer/presentation/customer_tab.dart';
import '../features/mine/presentation/mine_tab.dart';
import '../features/project/presentation/project_tab.dart';
import '../features/workbench/presentation/workbench_tab.dart';
import '../shared/widgets/x_bottom_nav.dart';
import 'tab_index.dart';
/// 4 个 Tab 的常驻容器,对应 `pages/tabs/index.vue`。
///
/// 源项目用 `v-show` 切换,不销毁重建、保留滚动位置与内部状态;
/// 这里用 [IndexedStack] 达到同样效果。
class TabsPage extends ConsumerWidget {
const TabsPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final current = ref.watch(tabIndexProvider);
return Scaffold(
backgroundColor: AppColors.bgPage,
body: Stack(
children: [
Positioned.fill(
child: IndexedStack(
index: current.index,
children: const [
WorkbenchTab(),
CustomerTab(),
ProjectTab(),
MineTab(),
],
),
),
Positioned(
left: 0,
right: 0,
bottom: 0,
child: XBottomNav(
current: current.key,
onChanged: (key) =>
ref.read(tabIndexProvider.notifier).selectByKey(key),
),
),
],
),
);
}
}