diff --git a/apps/flutter/components/lib/widgets/index.dart b/apps/flutter/components/lib/widgets/index.dart index a3d62f746..98afe17da 100644 --- a/apps/flutter/components/lib/widgets/index.dart +++ b/apps/flutter/components/lib/widgets/index.dart @@ -2,4 +2,5 @@ export 'avatar/index.dart'; export 'action-button/index.dart'; export 'back-to-top/index.dart'; export 'bottom-button/index.dart'; -export 'empty/index.dart'; \ No newline at end of file +export 'empty/index.dart'; +export 'menu/index.dart'; \ No newline at end of file diff --git a/apps/flutter/components/lib/widgets/menu/index.dart b/apps/flutter/components/lib/widgets/menu/index.dart new file mode 100644 index 000000000..ff9f64846 --- /dev/null +++ b/apps/flutter/components/lib/widgets/menu/index.dart @@ -0,0 +1,87 @@ +import 'package:core/models/common.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +class Navigation extends StatefulWidget { + const Navigation({ + super.key, + this.activedMenu, + this.menus = const [], + this.onMenuExpanded, + }); + + final List menus; + final String? activedMenu; + final void Function(Menu menu)? onMenuExpanded; + + @override + State createState() => _NavigationState(); +} + +class _NavigationState extends State { + @override + Widget build(BuildContext context) { + return _renderNavigations(widget.menus); + } + + Widget _renderNavigations(List menus) { + var mainMenus = menus.where((menu) => menu.children?.isNotEmpty == false); + var subMenus = menus.where((menu) => menu.children?.isNotEmpty == true); + + return Column( + children: [ + _renderMenus(subMenus.toList()), + ...mainMenus.map((menu) => _buildMenuItem(menu)), + ], + ); + } + + Widget _renderMenus(List menus) { + return ExpansionPanelList.radio( + initialOpenPanelValue: widget.activedMenu, + expandedHeaderPadding: const EdgeInsets.all(0), + expansionCallback: (panelIndex, isExpanded) { + if (widget.onMenuExpanded != null) { + widget.onMenuExpanded!(menus[panelIndex]); + } + }, + children: menus.map((Menu menu) { + var body = menu.children?.isNotEmpty == true + ? _renderNavigations(menu.children!) + : _buildMenuItem(menu); + return ExpansionPanelRadio( + canTapOnHeader: true, + headerBuilder: (BuildContext context, bool isExpanded) { + return SizedBox( + height: 30, + child: ListTile( + title: Text( + (menu.meta?['displayName']?.toString().tr ?? menu.displayName).padLeft(menu.level * 4)), + ), + ); + }, + body: body, + value: menu.name, + ); + }).toList(), + ); + } + + Widget _buildMenuItem(Menu menu) { + return InkWell( + onTap: () { + Get.toNamed(menu.path); + }, + child: FractionallySizedBox( + widthFactor: 1, + child: Container( + height: 30, + margin: const EdgeInsets.only(top: 10), + child: Text( + (menu.meta?['displayName']?.toString().tr ?? menu.displayName).padLeft(menu.level * 8), + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/apps/flutter/components/lib/widgets/menu/state.dart b/apps/flutter/components/lib/widgets/menu/state.dart new file mode 100644 index 000000000..2d3ff8350 --- /dev/null +++ b/apps/flutter/components/lib/widgets/menu/state.dart @@ -0,0 +1,3 @@ +class MenuState { + +} \ No newline at end of file diff --git a/apps/flutter/core/lib/models/common.dart b/apps/flutter/core/lib/models/common.dart index 2573eacc6..29cb93da9 100644 --- a/apps/flutter/core/lib/models/common.dart +++ b/apps/flutter/core/lib/models/common.dart @@ -42,3 +42,26 @@ class SignalrMessage { String method; List data; } + +class Menu { + Menu({ + required this.path, + required this.name, + required this.displayName, + this.id = 0, + this.level = 0, + this.description, + this.redirect, + this.meta, + this.children, + }); + String path; + String name; + String displayName; + String? description; + String? redirect; + int level; + int id; + Map? meta; + List? children; +} diff --git a/apps/flutter/core/lib/models/environment.dart b/apps/flutter/core/lib/models/environment.dart index 0ac766486..f99127ebd 100644 --- a/apps/flutter/core/lib/models/environment.dart +++ b/apps/flutter/core/lib/models/environment.dart @@ -177,10 +177,12 @@ class LocalizationConfig { this.defaultLanguage, this.useLocalResources = true, this.supportedLocales = const [], + this.translationFiles = const {}, }); String? defaultLanguage; bool? useLocalResources; List? supportedLocales; + Map>? translationFiles; factory LocalizationConfig.fromJson(Map json) => _$LocalizationConfigFromJson(json); Map toJson() => _$LocalizationConfigToJson(this); diff --git a/apps/flutter/core/lib/models/environment.g.dart b/apps/flutter/core/lib/models/environment.g.dart index 07efcf04e..45c7cf6fc 100644 --- a/apps/flutter/core/lib/models/environment.g.dart +++ b/apps/flutter/core/lib/models/environment.g.dart @@ -102,6 +102,11 @@ LocalizationConfig _$LocalizationConfigFromJson(Map json) => supportedLocales: json['supportedLocales'] != null ? (json['supportedLocales'] as List).map((e) => LanguageInfo.fromJson(e)).toList() : null, + translationFiles: json['translationFiles'] != null + ? (json['translationFiles'] as Map) + .map((key, value) => MapEntry(key, (value as List) + .map((e) => e as String).toList())) + : null, ); Map _$LocalizationConfigToJson(LocalizationConfig instance) => @@ -109,6 +114,7 @@ Map _$LocalizationConfigToJson(LocalizationConfig instance) => 'defaultLanguage': instance.defaultLanguage, 'useLocalResources': instance.useLocalResources, 'supportedLocales': instance.supportedLocales, + 'translationFiles': instance.translationFiles, }; RemoteService _$RemoteServiceFromJson(Map json) => diff --git a/apps/flutter/core/lib/utils/color.utils.dart b/apps/flutter/core/lib/utils/color.utils.dart new file mode 100644 index 000000000..65eb7943f --- /dev/null +++ b/apps/flutter/core/lib/utils/color.utils.dart @@ -0,0 +1,16 @@ +import 'dart:ui'; + +class ColorUtils { + static Color fromHex(String hexString) { + final buffer = StringBuffer(); + if (hexString.length == 6 || hexString.length == 7) buffer.write('ff'); + buffer.write(hexString.replaceFirst('#', '')); + return Color(int.parse(buffer.toString(), radix: 16)); + } +} + +extension HexStringToColor on String { + Color toColor() { + return ColorUtils.fromHex(this); + } +} \ No newline at end of file diff --git a/apps/flutter/core/lib/utils/index.dart b/apps/flutter/core/lib/utils/index.dart index fe2eb5beb..602f60d90 100644 --- a/apps/flutter/core/lib/utils/index.dart +++ b/apps/flutter/core/lib/utils/index.dart @@ -1,3 +1,4 @@ +export 'color.utils.dart'; export 'environment.utils.dart'; export 'internal.store.dart'; export 'localization.utils.dart'; diff --git a/apps/flutter/dev_app/.gitignore b/apps/flutter/dev_app/.gitignore index bceef1d48..0333a3f0e 100644 --- a/apps/flutter/dev_app/.gitignore +++ b/apps/flutter/dev_app/.gitignore @@ -44,4 +44,8 @@ app.*.map.json /android/app/release # Environment config -/res/config/development.json \ No newline at end of file +/res/config/development.json + +# Ignored translations +/res/translations/merge-en.json +/res/translations/merge-zh-Hans.json \ No newline at end of file diff --git a/apps/flutter/dev_app/lib/main.dart b/apps/flutter/dev_app/lib/main.dart index f99928b23..1ee8c93b6 100644 --- a/apps/flutter/dev_app/lib/main.dart +++ b/apps/flutter/dev_app/lib/main.dart @@ -2,10 +2,12 @@ import 'package:core/dependency/index.dart'; import 'package:core/utils/theme.utils.dart'; import 'package:core/utils/logging.dart'; import 'package:dev_app/main.module.dart'; +import 'package:dev_app/pages/index.dart'; +import 'package:dev_app/pages/public/route.name.dart'; import 'package:dev_app/utils/localization.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:flutter/material.dart'; import 'package:get/get.dart'; Future main() async { @@ -25,8 +27,9 @@ class MyApp extends StatelessWidget { theme: ThemeUtils.lightTheme, darkTheme: ThemeUtils.darkTheme, themeMode: ThemeMode.system, - initialRoute: '/', + initialRoute: PublicRoutes.main, getPages: module.getRoutes(), + unknownRoute: PublicRoute.notFound, debugShowMaterialGrid: false, enableLog: true, builder: EasyLoading.init(), diff --git a/apps/flutter/dev_app/lib/main.module.dart b/apps/flutter/dev_app/lib/main.module.dart index a0dc8c9b3..ed7fca5a9 100644 --- a/apps/flutter/dev_app/lib/main.module.dart +++ b/apps/flutter/dev_app/lib/main.module.dart @@ -1,5 +1,6 @@ import 'package:components/index.dart'; import 'package:dev_app/pages/index.dart'; +import 'package:dev_app/pages/public/route.name.dart'; import 'package:dev_app/services/index.dart'; import 'package:dev_app/utils/initial.utils.dart'; import 'package:dev_app/utils/loading.dart'; @@ -30,7 +31,7 @@ class MainModule extends Module { @override List get routes => [ GetPage( - name: '/', + name: PublicRoutes.main, page: () => const MainPage(), bindings: [ MainBinding(), diff --git a/apps/flutter/dev_app/lib/pages/main/controller.dart b/apps/flutter/dev_app/lib/pages/main/controller.dart index ccfccf474..7cd35ca98 100644 --- a/apps/flutter/dev_app/lib/pages/main/controller.dart +++ b/apps/flutter/dev_app/lib/pages/main/controller.dart @@ -1,13 +1,13 @@ import 'package:core/dependency/index.dart'; import 'package:core/abstracts/signalr.service.dart'; import 'package:core/services/environment.service.dart'; -import 'package:core/services/notification.send.service.dart'; import 'package:dev_app/handlers/index.dart'; import 'package:get/get.dart'; import 'package:core/services/session.service.dart'; import 'package:core/services/subscription.service.dart'; import 'package:core/utils/index.dart'; import 'package:notifications/models/index.dart'; +import 'package:notifications/services/notification.state.service.dart'; import 'package:notifications/tokens/index.dart'; class MainController extends GetxController { @@ -17,7 +17,7 @@ class MainController extends GetxController { SessionService get _sessionService => injector.get(); SubscriptionService get _subscriptionService => injector.get(tag: NotificationTokens.consumer); SignalrService get _signalrService => injector.get(tag: NotificationTokens.producer); - NotificationSendService get _notificationSendService => injector.get(); + NotificationStateService get _notificationStateService => injector.get(); EnvironmentService get _environmentService => injector.get(); ErrorHandler get _errorHandler => injector.get(); @@ -37,14 +37,7 @@ class MainController extends GetxController { if (data == null) continue; // 解析通知数据 var notification = NotificationInfo.fromJson(data as dynamic); - // 格式化为移动端可识别通知数据 - var payload = NotificationPaylod.fromNotification(notification); - // 发布本地通知 - await _notificationSendService.send( - payload.title, - payload.body, - payload.payload, - ); + _notificationStateService.addNotification(notification); } }, ); diff --git a/apps/flutter/dev_app/lib/pages/public/center/controller.dart b/apps/flutter/dev_app/lib/pages/public/center/controller.dart index 653c1a1f7..c40367fd5 100644 --- a/apps/flutter/dev_app/lib/pages/public/center/controller.dart +++ b/apps/flutter/dev_app/lib/pages/public/center/controller.dart @@ -44,15 +44,15 @@ class CenterController extends GetxController { } void onClickFeedback() { - + redirectToRoute('/feedback'); } void onClickHelp() { - + redirectToRoute('/help'); } void onClickInfo() { - + redirectToRoute('/info'); } void onClickMessage() { diff --git a/apps/flutter/dev_app/lib/pages/public/error/index.dart b/apps/flutter/dev_app/lib/pages/public/error/index.dart new file mode 100644 index 000000000..aa4da5d13 --- /dev/null +++ b/apps/flutter/dev_app/lib/pages/public/error/index.dart @@ -0,0 +1 @@ +export './not_found/index.dart'; \ No newline at end of file diff --git a/apps/flutter/dev_app/lib/pages/public/error/not_found/index.dart b/apps/flutter/dev_app/lib/pages/public/error/not_found/index.dart new file mode 100644 index 000000000..3205d6a40 --- /dev/null +++ b/apps/flutter/dev_app/lib/pages/public/error/not_found/index.dart @@ -0,0 +1 @@ +export 'view.dart'; \ No newline at end of file diff --git a/apps/flutter/dev_app/lib/pages/public/error/not_found/view.dart b/apps/flutter/dev_app/lib/pages/public/error/not_found/view.dart new file mode 100644 index 000000000..e25c60e1a --- /dev/null +++ b/apps/flutter/dev_app/lib/pages/public/error/not_found/view.dart @@ -0,0 +1,23 @@ +import 'package:bruno/bruno.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +class PageNotFound extends StatelessWidget { + const PageNotFound({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text("404Message".tr) + ), + body: BrnAbnormalStateWidget( + img: Image.asset( + 'res/images/no_data.png', + scale: 3.0, + ), + content: "404MessageDetail".tr, + ), + ); + } +} \ No newline at end of file diff --git a/apps/flutter/dev_app/lib/pages/public/home/controller.dart b/apps/flutter/dev_app/lib/pages/public/home/controller.dart index 0e8bd7cdd..9b272b415 100644 --- a/apps/flutter/dev_app/lib/pages/public/home/controller.dart +++ b/apps/flutter/dev_app/lib/pages/public/home/controller.dart @@ -1,5 +1,7 @@ +import 'package:core/models/common.dart'; import 'package:get/get.dart'; import 'package:core/dependency/index.dart'; +import 'package:notifications/services/notification.state.service.dart'; import 'package:platforms/services/index.dart'; import 'state.dart'; @@ -7,6 +9,7 @@ import 'state.dart'; class HomeController extends GetxController { MenuStateService get _menuStateService => injector.get(); FavoriteMenuStateService get _favoriteMenuStateService => injector.get(); + NotificationStateService get _notificationStateService => injector.get(); final Rx _state = Rx(HomeState()); HomeState get state => _state.value; @@ -14,17 +17,39 @@ class HomeController extends GetxController { @override void onInit() { super.onInit(); - // _menuStateService.getMyMenus$() - // .listen((menus) { - // _state.update((val) { - // val?.menus = menus; - // }); - // }); - // _favoriteMenuStateService.getFavoriteMenus$() - // .listen((menus) { - // _state.update((val) { - // val?.favoriteMenus = menus; - // }); - // }); + _menuStateService.getMyMenus$() + .listen((menus) { + _state.update((val) { + val?.menus = menus; + }); + }); + _favoriteMenuStateService.getFavoriteMenus$() + .listen((menus) { + _state.update((val) { + val?.favoriteMenus = menus; + }); + }); + _notificationStateService.getNotifications$() + .listen((payload) { + var notifications = state.notifications.reversed.take(5).toList(); + notifications.add(payload); + _state.update((val) { + val?.notifications = notifications; + }); + }); + } + + Future refreshMenus() async { + await _menuStateService.refreshState(); + } + + void redirectToRoute(String route) { + Get.toNamed(route); + } + + void onMenuExpanded(Menu menu) { + _state.update((val) { + val?.activedMenu = menu.name; + }); } } \ No newline at end of file diff --git a/apps/flutter/dev_app/lib/pages/public/home/state.dart b/apps/flutter/dev_app/lib/pages/public/home/state.dart index a810abe02..4ddaa2a60 100644 --- a/apps/flutter/dev_app/lib/pages/public/home/state.dart +++ b/apps/flutter/dev_app/lib/pages/public/home/state.dart @@ -1,10 +1,38 @@ +import 'package:core/models/common.dart'; +import 'package:notifications/models/common.dart'; import 'package:platforms/modes/menu.dto.dart'; class HomeState { HomeState({ + this.activedMenu, this.menus = const [], this.favoriteMenus = const [], + this.notifications = const [], }); + String? activedMenu; List menus; List favoriteMenus; + List notifications; + + List getMenus() => _buildTreeRecursive(menus, null, 0); + + List _buildTreeRecursive(List treeMenus, String? parentId, int level) { + List results = []; + var tempList = treeMenus.where((menu) => menu.parentId == parentId).toList(); + for (int i = 0; i < tempList.length; i++) { + var menu = Menu( + id: tempList[i].id.hashCode, + path: tempList[i].path, + name: tempList[i].name, + displayName: tempList[i].displayName, + description: tempList[i].description, + redirect: tempList[i].redirect, + meta: tempList[i].meta, + level: level + 1 + ); + menu.children = _buildTreeRecursive(treeMenus, tempList[i].id, menu.level); + results.add(menu); + } + return results; + } } \ No newline at end of file diff --git a/apps/flutter/dev_app/lib/pages/public/home/view.dart b/apps/flutter/dev_app/lib/pages/public/home/view.dart index 5dcc92008..b55edaa17 100644 --- a/apps/flutter/dev_app/lib/pages/public/home/view.dart +++ b/apps/flutter/dev_app/lib/pages/public/home/view.dart @@ -1,8 +1,13 @@ +import 'package:account/pages/route.name.dart'; import 'package:components/index.dart'; -import 'package:dev_app/pages/public/home/widget/search.dart'; +import 'package:core/utils/index.dart'; +import 'package:dev_app/pages/system/route.name.dart'; +import 'package:flex_color_scheme/flex_color_scheme.dart'; import 'package:flutter/material.dart'; +import 'package:get/get.dart'; import 'controller.dart'; +import './widget/index.dart'; class HomePage extends BasePage { const HomePage({super.key}); @@ -20,26 +25,98 @@ class HomePage extends BasePage { onPressed: () { showSearch(context: context, delegate: SearchBarDelegate(menus: bloc.state.menus)); }, - child: const Row( + child: Row( children: [ - Icon(Icons.search), - Expanded(child: Text('搜索功能')) + const Icon(Icons.search), + Expanded(child: Text('Label:SearchFeatures'.tr)) ], ), ), ), - body: Column( + body: ListView( children: [ - Expanded( - child: ListView.builder( - itemCount: bloc.state.favoriteMenus.length, - itemBuilder: (context, index) { - var favoriteMenu = bloc.state.favoriteMenus[index]; - return Text(favoriteMenu.displayName ?? favoriteMenu.name); - }, + Obx(() => NotificationBar(notifications: bloc.state.notifications)), + QuickNavigation( + menus: [ + _buildMenu( + SystemRoutes.settings, + SystemRoutes.settings, + icon: 'res/images/setting.png', + displayName: "Label:SystemSettings".tr, + color: Colors.red.hex), + _buildMenu( + AccountRoutes.profile, + AccountRoutes.profile, + icon: 'res/images/profile.png', + displayName: "Page:UserProfile".tr, + color: const Color.fromARGB(255, 68, 160, 206).hex), + ], ), - ), - ], + Obx(() => MyFavorite( + favoriteMenus: bloc.state.favoriteMenus, + favoriteMenuBuilder: (favoriteMenu) { + return _buildMenu( + favoriteMenu.name, + favoriteMenu.path, + aliasName: favoriteMenu.aliasName, + //icon: favoriteMenu.icon, + // TODO: 需要各个模块自行提供本地图标 + icon: 'res/images/setting.png', + color: favoriteMenu.color, + displayName: favoriteMenu.displayName, + ); + }, + )), + ], + ), + drawer: SafeArea( + child: Obx(() => MenuDrawer( + activedMenu: bloc.state.activedMenu, + menus: bloc.state.getMenus(), + onMenuExpanded: bloc.onMenuExpanded, + onMenuRefresh: bloc.refreshMenus, + )), + ), + ); + } + + Widget _buildMenu( + String name, + String path, + { + String? aliasName, + String? icon, + String? color, + String? displayName, + } + ) { + return InkWell( + onTap: () { + bloc.redirectToRoute(path); + }, + child: SizedBox( + height: 20, + width: 30, + child: Column( + children: [ + const SizedBox(height: 10), + icon != null + ? Image.asset( + icon, + height: 40, + width: 40, + color: color.isNullOrWhiteSpace() ? null : ColorUtils.fromHex(color!), + ) + : Empty.none, + Text( + displayName ?? aliasName ?? name, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 14 + ), + ) + ], + ), ), ); } diff --git a/apps/flutter/dev_app/lib/pages/public/home/widget/index.dart b/apps/flutter/dev_app/lib/pages/public/home/widget/index.dart new file mode 100644 index 000000000..769be9109 --- /dev/null +++ b/apps/flutter/dev_app/lib/pages/public/home/widget/index.dart @@ -0,0 +1,5 @@ +export 'search.dart'; +export 'menu_drawer.dart'; +export 'my_favorite.dart'; +export 'notification_bar.dart'; +export 'quick_navigation.dart'; \ No newline at end of file diff --git a/apps/flutter/dev_app/lib/pages/public/home/widget/menu_drawer.dart b/apps/flutter/dev_app/lib/pages/public/home/widget/menu_drawer.dart new file mode 100644 index 000000000..2fd2a49f4 --- /dev/null +++ b/apps/flutter/dev_app/lib/pages/public/home/widget/menu_drawer.dart @@ -0,0 +1,76 @@ +import 'package:components/widgets/menu/index.dart'; +import 'package:core/models/common.dart'; +import 'package:flutter/material.dart'; + +class MenuDrawer extends StatelessWidget { + const MenuDrawer({ + super.key, + this.activedMenu, + this.menus = const [], + required this.onMenuRefresh, + this.onMenuExpanded, + }); + + final String? activedMenu; + final List menus; + final void Function(Menu menu)? onMenuExpanded; + final Future Function() onMenuRefresh; + + @override + Widget build(BuildContext context) { + return RefreshIndicator( + onRefresh: onMenuRefresh, + child: Drawer( + width: 260, + child: Column( + children: [ + _buildLogo(), + Expanded( + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: Column( + children: [ + Navigation( + activedMenu: activedMenu, + menus: menus, + onMenuExpanded: onMenuExpanded, + ), + ], + ), + ) + ), + ], + ), + ), + ); + } + + Widget _buildLogo() { + return Container( + height: 24, + margin: const EdgeInsets.all(10), + child: Row( + children: [ + Padding( + padding: const EdgeInsets.only(left: 10), + child: Image.asset( + 'res/images/logo.png', + height: 20, + width: 20, + ), + ), + const Padding( + padding: EdgeInsets.only(left: 10), + child: Text( + 'abp flutter', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w400, + ), + ), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/apps/flutter/dev_app/lib/pages/public/home/widget/my_favorite.dart b/apps/flutter/dev_app/lib/pages/public/home/widget/my_favorite.dart new file mode 100644 index 000000000..ef2c0984d --- /dev/null +++ b/apps/flutter/dev_app/lib/pages/public/home/widget/my_favorite.dart @@ -0,0 +1,42 @@ +import 'package:components/widgets/empty/index.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:platforms/modes/menu.dto.dart'; + +class MyFavorite extends StatelessWidget { + const MyFavorite({ + super.key, + required this.favoriteMenus, + required this.favoriteMenuBuilder, + }); + + final List favoriteMenus; + final Widget Function(UserFavoriteMenuDto favoriteMenu) favoriteMenuBuilder; + + @override + Widget build(BuildContext context) { + return ExpansionTile( + initiallyExpanded: true, + title: Text('Label:MyFavorite'.tr, + style: Theme.of(context).textTheme.titleMedium, + ), + children: [ + GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: favoriteMenus.length, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 4, + crossAxisSpacing: 5, + ), + itemBuilder: (BuildContext context, int index) { + if (index >= favoriteMenus.length) { + return Empty.none; + } + return favoriteMenuBuilder(favoriteMenus[index]); + }, + ), + ], + ); + } +} \ No newline at end of file diff --git a/apps/flutter/dev_app/lib/pages/public/home/widget/notification_bar.dart b/apps/flutter/dev_app/lib/pages/public/home/widget/notification_bar.dart new file mode 100644 index 000000000..b7272fc64 --- /dev/null +++ b/apps/flutter/dev_app/lib/pages/public/home/widget/notification_bar.dart @@ -0,0 +1,55 @@ +import 'package:bruno/bruno.dart'; +import 'package:components/widgets/empty/index.dart'; +import 'package:flutter/material.dart'; +import 'package:notifications/models/common.dart'; +import 'package:notifications/models/notification.dart'; + +class NotificationBar extends StatelessWidget { + const NotificationBar({ + super.key, + required this.notifications + }); + + final List notifications; + + @override + Widget build(BuildContext context) { + if (notifications.isEmpty) { + return Empty.none; + } + return SizedBox( + height: 40, + child: SingleChildScrollView( + child: Column( + children: notifications.map((payload) { + return BrnNoticeBar( + padding: const EdgeInsets.only(left: 5, right: 5, top: 3), + leftWidget: Image.asset( + 'res/images/notification.png', + height: 30, + width: 30, + ), + content: payload.title, + marquee: true, + noticeStyle: _mapNoticeStyles(payload.severity), + ); + }).toList(), + ), + ), + ); + } + + NoticeStyle _mapNoticeStyles(NotificationSeverity? severity) { + if (severity == null) return NoticeStyles.normalNoticeWithArrow; + switch (severity) { + case NotificationSeverity.info: + case NotificationSeverity.success: + return NoticeStyles.succeedWithArrow; + case NotificationSeverity.fatal: + case NotificationSeverity.error: + return NoticeStyles.failWithArrow; + case NotificationSeverity.warn: + return NoticeStyles.warningWithArrow; + } + } +} \ No newline at end of file diff --git a/apps/flutter/dev_app/lib/pages/public/home/widget/quick_navigation.dart b/apps/flutter/dev_app/lib/pages/public/home/widget/quick_navigation.dart new file mode 100644 index 000000000..929bb3c10 --- /dev/null +++ b/apps/flutter/dev_app/lib/pages/public/home/widget/quick_navigation.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +class QuickNavigation extends StatelessWidget { + const QuickNavigation({ + super.key, + this.menus = const [], + }); + + final List menus; + + @override + Widget build(BuildContext context) { + return ExpansionTile( + initiallyExpanded: true, + title: Text('Label:QuickNavigation'.tr, + style: Theme.of(context).textTheme.titleMedium, + ), + children: [ + SizedBox( + height: 120, + child: GridView.count( + shrinkWrap: true, + crossAxisCount: 4, + crossAxisSpacing: 5, + physics: const NeverScrollableScrollPhysics(), + children: menus, + ), + ), + ], + ); + } +} \ No newline at end of file diff --git a/apps/flutter/dev_app/lib/pages/public/home/widget/search.dart b/apps/flutter/dev_app/lib/pages/public/home/widget/search.dart index 99c8e4998..46baf8ae6 100644 --- a/apps/flutter/dev_app/lib/pages/public/home/widget/search.dart +++ b/apps/flutter/dev_app/lib/pages/public/home/widget/search.dart @@ -13,8 +13,7 @@ class SearchBarDelegate extends SearchDelegate { List? buildActions(BuildContext context) { Widget button = IconButton( onPressed: () { - query = ""; - showSuggestions(context); + close(context, "error"); }, icon: const Icon(Icons.clear), ); @@ -24,15 +23,17 @@ class SearchBarDelegate extends SearchDelegate { @override Widget? buildLeading(BuildContext context) { - return IconButton( - onPressed: () { - close(context, "error"); - }, - icon: AnimatedIcon( - icon: AnimatedIcons.menu_arrow, - progress: transitionAnimation, - ), - ); + // return IconButton( + // onPressed: () { + // query = ""; + // showSuggestions(context); + // }, + // icon: AnimatedIcon( + // icon: AnimatedIcons.menu_arrow, + // progress: transitionAnimation, + // ), + // ); + return null; } @override diff --git a/apps/flutter/dev_app/lib/pages/public/index.dart b/apps/flutter/dev_app/lib/pages/public/index.dart index aee3141ae..7d24fe4b5 100644 --- a/apps/flutter/dev_app/lib/pages/public/index.dart +++ b/apps/flutter/dev_app/lib/pages/public/index.dart @@ -1,3 +1,4 @@ export './center/index.dart'; export './home/index.dart'; -export './work/index.dart'; \ No newline at end of file +export './work/index.dart'; +export './error/index.dart'; \ No newline at end of file diff --git a/apps/flutter/dev_app/lib/pages/public/route.name.dart b/apps/flutter/dev_app/lib/pages/public/route.name.dart index 4361f2488..2f5830c89 100644 --- a/apps/flutter/dev_app/lib/pages/public/route.name.dart +++ b/apps/flutter/dev_app/lib/pages/public/route.name.dart @@ -1,5 +1,7 @@ class PublicRoutes { + static String main = '/main'; static String home = '/home'; static String work = '/work'; static String center = '/center'; + static String notFound = '/error/not_found'; } \ No newline at end of file diff --git a/apps/flutter/dev_app/lib/pages/public/route.public.dart b/apps/flutter/dev_app/lib/pages/public/route.public.dart index b4d6a39f5..9ade2f155 100644 --- a/apps/flutter/dev_app/lib/pages/public/route.public.dart +++ b/apps/flutter/dev_app/lib/pages/public/route.public.dart @@ -4,7 +4,13 @@ import 'index.dart'; import 'route.name.dart'; class PublicRoute { + static GetPage notFound = GetPage( + name: PublicRoutes.notFound, + page: () => const PageNotFound(), + ); + static List routes = [ + notFound, GetPage( name: PublicRoutes.home, page: () => const HomePage(), diff --git a/apps/flutter/dev_app/lib/services/notification.send.local.service.dart b/apps/flutter/dev_app/lib/services/notification.send.local.service.dart index beddf7184..de9191cc8 100644 --- a/apps/flutter/dev_app/lib/services/notification.send.local.service.dart +++ b/apps/flutter/dev_app/lib/services/notification.send.local.service.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:core/index.dart'; import 'package:get/get.dart'; +import 'package:notifications/services/notification.state.service.dart'; import 'package:rxdart/rxdart.dart' hide Notification; import 'package:core/models/notifications.dart'; @@ -14,6 +15,22 @@ class FlutterLocalNotificationsSendService extends NotificationSendService { final Subject _selectedNotifications$ = BehaviorSubject(); EnvironmentService get _environmentService => resolve(); + NotificationStateService get _notificationStateService => resolve(); + + @override + void onInit() { + super.onInit(); + _notificationStateService + .getNotifications$() + .listen((payload) async { + // 发布本地通知 + await send( + payload.title, + payload.body, + payload.payload, + ); + }); + } Future initAsync() async { var environment = _environmentService.getEnvironment(); diff --git a/apps/flutter/dev_app/lib/services/translation.service.res.service.dart b/apps/flutter/dev_app/lib/services/translation.service.res.service.dart index 82a9dfe84..28ef8e23f 100644 --- a/apps/flutter/dev_app/lib/services/translation.service.res.service.dart +++ b/apps/flutter/dev_app/lib/services/translation.service.res.service.dart @@ -1,5 +1,6 @@ import 'dart:ui'; +import 'package:core/services/environment.service.dart'; import 'package:core/services/localization.service.dart'; import 'package:core/services/service.base.dart'; import 'package:core/services/session.service.dart'; @@ -17,6 +18,7 @@ class TranslationResService extends ServiceBase implements TranslationService { final InternalStore _store = InternalStore(state: TranslationState()); SessionService get _sessionService => resolve(); + EnvironmentService get _environmentService => resolve(); LocalizationService get _localizationService => resolve(); @override @@ -44,13 +46,21 @@ class TranslationResService extends ServiceBase implements TranslationService { Future _mapTranslationsMap(String language) async { Map> translationsMap = {}; - var filePath = 'res/translations/$language.json'; - var content = await rootBundle.loadString(filePath); - var translationsObject = jsonDecode(content) as Map; - translationsMap.putIfAbsent( - language, - () => translationsObject.map((key, value) => MapEntry(key, value)) - ); + var environment = _environmentService.getEnvironment(); + var translationFiles = environment.localization.translationFiles?[language] ?? ['$language.json']; + + for (var translationFile in translationFiles) { + try { + var filePath = 'res/translations/$translationFile'; + var content = await rootBundle.loadString(filePath); + var translationsObject = jsonDecode(content) as Map; + var translations = translationsMap[language] ?? {}; + translations.addAll(translationsObject.map((key, value) => MapEntry(key, value))); + translationsMap.putIfAbsent(language, () => translations); + } catch (e) { + logger.error(e); + } + } return TranslationState( language: language, translations: translationsMap, diff --git a/apps/flutter/dev_app/pubspec.lock b/apps/flutter/dev_app/pubspec.lock index bb2298370..bd7d65046 100644 --- a/apps/flutter/dev_app/pubspec.lock +++ b/apps/flutter/dev_app/pubspec.lock @@ -40,6 +40,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.11.0" + bindings_compatible: + dependency: transitive + description: + name: bindings_compatible + sha256: "5dd5189f7512aff8ec180a8a11bd59230aa34a2d743e65e427192b7292a78d87" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.1" boolean_selector: dependency: transitive description: @@ -48,6 +56,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.1.1" + bruno: + dependency: "direct main" + description: + name: bruno + sha256: "8bd461a658996000eab1111a93fb4826ade878103f5a9afa29a414046805448b" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.4.1" build: dependency: transitive description: @@ -307,6 +323,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "3.0.5" + flutter_easyrefresh: + dependency: transitive + description: + name: flutter_easyrefresh + sha256: "5d161ee5dcac34da9065116568147d742dd25fb9bff3b10024d9054b195087ad" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.2" flutter_highlight: dependency: transitive description: @@ -514,6 +538,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.2.0" + lpinyin: + dependency: transitive + description: + name: lpinyin + sha256: "0bb843363f1f65170efd09fbdfc760c7ec34fc6354f9fcb2f89e74866a0d814a" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.3" markdown: dependency: transitive description: @@ -608,6 +640,22 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.8.3" + path_drawing: + dependency: transitive + description: + name: path_drawing + sha256: bbb1934c0cbb03091af082a6389ca2080345291ef07a5fa6d6e078ba8682f977 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.1" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.1" path_provider: dependency: transitive description: @@ -664,6 +712,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "5.4.0" + photo_view: + dependency: transitive + description: + name: photo_view + sha256: "8036802a00bae2a78fc197af8a158e3e2f7b500561ed23b4c458107685e645bb" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.14.0" platform: dependency: transitive description: diff --git a/apps/flutter/dev_app/pubspec.yaml b/apps/flutter/dev_app/pubspec.yaml index 7336089e5..db627ecaf 100644 --- a/apps/flutter/dev_app/pubspec.yaml +++ b/apps/flutter/dev_app/pubspec.yaml @@ -49,6 +49,7 @@ dependencies: platforms: path: '../platform' + bruno: ^3.3.0 dio: ^5.2.0+1 flutter_easyloading: ^3.0.5 flutter_picker: ^2.1.0 diff --git a/apps/flutter/dev_app/res/config/demo.json b/apps/flutter/dev_app/res/config/demo.json index 49b54960f..840abdf4e 100644 --- a/apps/flutter/dev_app/res/config/demo.json +++ b/apps/flutter/dev_app/res/config/demo.json @@ -13,6 +13,20 @@ "localization": { "useLocalResources": true, "defaultLanguage": "zh-Hans", + "translationFiles": { + "zh-Hans": [ + "zh-Hans.json" + ], + "zh_CN": [ + "zh-Hans.json" + ], + "en": [ + "en.json" + ], + "en_US": [ + "en.json" + ] + }, "supportedLocales": [ { "cultureName": "en", diff --git a/apps/flutter/dev_app/res/images/no_data.png b/apps/flutter/dev_app/res/images/no_data.png new file mode 100644 index 000000000..10d07379d Binary files /dev/null and b/apps/flutter/dev_app/res/images/no_data.png differ diff --git a/apps/flutter/dev_app/res/images/notification.png b/apps/flutter/dev_app/res/images/notification.png new file mode 100644 index 000000000..bf3be84b1 Binary files /dev/null and b/apps/flutter/dev_app/res/images/notification.png differ diff --git a/apps/flutter/dev_app/res/images/profile.png b/apps/flutter/dev_app/res/images/profile.png new file mode 100644 index 000000000..0d8bf21d4 Binary files /dev/null and b/apps/flutter/dev_app/res/images/profile.png differ diff --git a/apps/flutter/dev_app/res/images/setting.png b/apps/flutter/dev_app/res/images/setting.png new file mode 100644 index 000000000..46c201588 Binary files /dev/null and b/apps/flutter/dev_app/res/images/setting.png differ diff --git a/apps/flutter/dev_app/res/translations/en.json b/apps/flutter/dev_app/res/translations/en.json index 602e12384..e52514ca9 100644 --- a/apps/flutter/dev_app/res/translations/en.json +++ b/apps/flutter/dev_app/res/translations/en.json @@ -5,6 +5,7 @@ "404Message": "Page not found", "404MessageDetail": "Sorry, there's nothing at this address.", "500Message": "Internal Server Error", + "Avatar": "Avatar", "Center:Feedback": "Feedback", "Center:Help": "Help", "Center:Info": "Info", @@ -12,6 +13,9 @@ "Center:Settings": "Settings", "DisplayName:Abp.Localization.DefaultLanguage": "Default language", "Description:Abp.Localization.DefaultLanguage": "The default language of the application.", + "DisplayName:UserName": "User Name", + "DisplayName:Email": "Email", + "DisplayName:PhoneNumber": "Phone Number", "DefaultErrorMessage": "An error has occurred!", "DefaultErrorMessageDetail": "Error detail not sent by server.", "DefaultErrorMessage401": "You are not authenticated!", @@ -37,6 +41,7 @@ "Label:NotifierSettings": "Notifier Settings", "Label:PasswordRequired": "Please enter password", "Label:PhoneNumberNotBound": "PhoneNumber Not Bound", + "Label:SearchFeatures": "Search Features", "Label:Submit": "Submit", "Label:SwitchTheme": "Switch Theme", "Label:SystemSettings": "System Settings", @@ -45,6 +50,8 @@ "Label:Cancel": "Cancel", "Label:Confirm": "Confirm", "Label:LoginToPortal": "Login To Portal", + "Label:QuickNavigation": "Quick navigation", + "Label:MyFavorite": "My favorites", "Languages": "Languages", "Page:Center": "Center", "Page:Home": "Home", diff --git a/apps/flutter/dev_app/res/translations/zh-Hans.json b/apps/flutter/dev_app/res/translations/zh-Hans.json index bf9ae99b8..c5ad416ab 100644 --- a/apps/flutter/dev_app/res/translations/zh-Hans.json +++ b/apps/flutter/dev_app/res/translations/zh-Hans.json @@ -2,9 +2,10 @@ "401Message": "未授权", "403Message": "禁止访问", "403MessageDetail": "您没有权限执行此操作!", - "404Message": "网页未找到", + "404Message": "页面未找到", "404MessageDetail": "抱歉, 这个地址是空的.", "500Message": "内部服务器错误", + "Avatar": "头像", "Center:Feedback": "意见反馈", "Center:Help": "在线帮助", "Center:Info": "关于", @@ -12,6 +13,9 @@ "Center:Settings": "设置", "DisplayName:Abp.Localization.DefaultLanguage": "默认语言", "Description:Abp.Localization.DefaultLanguage": "应用程序的默认语言.", + "DisplayName:UserName": "用户名", + "DisplayName:Email": "邮件地址", + "DisplayName:PhoneNumber": "手机号码", "DefaultErrorMessage": "发生错误!", "DefaultErrorMessageDetail": "服务器未发送错误的详细信息.", "DefaultErrorMessage401": "未通过身份验证!", @@ -37,6 +41,7 @@ "Label:NotifierSettings": "通知设置", "Label:PasswordRequired": "请输入密码", "Label:PhoneNumberNotBound": "未绑定手机号", + "Label:SearchFeatures": "搜索功能", "Label:Submit": "提交", "Label:SwitchTheme": "切换主题", "Label:SystemSettings": "系统设置", @@ -45,6 +50,8 @@ "Label:Cancel": "取消", "Label:Confirm": "确认", "Label:LoginToPortal": "登录到门户", + "Label:QuickNavigation": "快捷导航", + "Label:MyFavorite": "我的收藏", "Languages": "语言", "Page:Center": "个人中心", "Page:Home": "首页", diff --git a/apps/flutter/notifications/lib/models/notification.state.dart b/apps/flutter/notifications/lib/models/notification.state.dart index 2b7882188..a45bfc9ac 100644 --- a/apps/flutter/notifications/lib/models/notification.state.dart +++ b/apps/flutter/notifications/lib/models/notification.state.dart @@ -14,6 +14,7 @@ class NotificationState { }); bool isEnabled; List groups; + NotificationGroup? findGroup(String name) { return groups.firstWhereOrNull((item) => item.name == name); diff --git a/apps/flutter/notifications/lib/services/notification.state.service.dart b/apps/flutter/notifications/lib/services/notification.state.service.dart index 037a588d5..3cfa8eb73 100644 --- a/apps/flutter/notifications/lib/services/notification.state.service.dart +++ b/apps/flutter/notifications/lib/services/notification.state.service.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'package:notifications/models/common.dart'; import 'package:rxdart/rxdart.dart' hide Notification; import 'package:notifications/models/notification.dart'; import 'package:core/services/session.service.dart'; @@ -20,6 +21,8 @@ class NotificationStateService extends ServiceBase { NotificationService get _notificationService => resolve(); SignalrService get _signalrService => resolve(tag: NotificationTokens.producer); + final BehaviorSubject _notifications = BehaviorSubject(); + final InternalStore _store = InternalStore( state: _initState() ); @@ -41,6 +44,10 @@ class NotificationStateService extends ServiceBase { return _store.sliceUpdate((state) => state); } + Stream getNotifications$() { + return _notifications; + } + NotificationGroup? findGroup(String name) { return _store.state.findGroup(name); } @@ -75,6 +82,10 @@ class NotificationStateService extends ServiceBase { return configState ?? NotificationState(isEnabled: true, groups: []); } + void addNotification(NotificationInfo notification) { + _notifications.add(NotificationPaylod.fromNotification(notification)); + } + Future> getGroupAndCombineWithNotification(List groupItems) { return _notificationService.getMySubscribedListAsync() .then((subscres) { diff --git a/apps/flutter/platform/lib/services/favorite.menu.state.service.dart b/apps/flutter/platform/lib/services/favorite.menu.state.service.dart index 1114b2403..c29255709 100644 --- a/apps/flutter/platform/lib/services/favorite.menu.state.service.dart +++ b/apps/flutter/platform/lib/services/favorite.menu.state.service.dart @@ -1,5 +1,6 @@ import 'package:core/services/environment.service.dart'; import 'package:core/services/service.base.dart'; +import 'package:core/services/session.service.dart'; import 'package:core/utils/index.dart'; import 'package:platforms/modes/state.dart'; import 'package:platforms/modes/menu.dto.dart'; @@ -10,18 +11,29 @@ class FavoriteMenuStateService extends ServiceBase { final InternalStore _state = InternalStore(state: FavoriteMenuState()); + SessionService get _sessionService => resolve(); EnvironmentService get _environmentService => resolve(); FavoriteMenuService get _favoriteMenuService => resolve(); @override void onInit() { super.onInit(); - refreshState(); + _initState(); + } + + void _initState() { + _sessionService.getToken$() + .listen((token) { + _state.patch((state) => state.menus = []); + if (token != null) { + refreshState(); + } + }); } Future refreshState() async { var environment = _environmentService.getEnvironment(); - var framework = environment.application.framework ?? 'flutter'; + var framework = environment.application.framework ?? 'abp-flutter'; var result = await _favoriteMenuService.getMyFavoriteMenuList(framework); _state.patch((state) => state.menus = result.items); } diff --git a/apps/flutter/platform/lib/services/menu.state.service.dart b/apps/flutter/platform/lib/services/menu.state.service.dart index d6575bb67..8729a72e8 100644 --- a/apps/flutter/platform/lib/services/menu.state.service.dart +++ b/apps/flutter/platform/lib/services/menu.state.service.dart @@ -1,5 +1,6 @@ import 'package:core/services/environment.service.dart'; import 'package:core/services/service.base.dart'; +import 'package:core/services/session.service.dart'; import 'package:core/utils/index.dart'; import 'package:platforms/modes/state.dart'; import 'package:platforms/modes/menu.dto.dart'; @@ -11,17 +12,28 @@ class MenuStateService extends ServiceBase { final InternalStore _state = InternalStore(state: MenuState()); EnvironmentService get _environmentService => resolve(); + SessionService get _sessionService => resolve(); MenuService get _menuService => resolve(); @override void onInit() { super.onInit(); - refreshState(); + _initState(); + } + + void _initState() { + _sessionService.getToken$() + .listen((token) { + _state.patch((state) => state.menus = []); + if (token != null) { + refreshState(); + } + }); } Future refreshState() async { var environment = _environmentService.getEnvironment(); - var framework = environment.application.framework ?? 'flutter'; + var framework = environment.application.framework ?? 'abp-flutter'; var result = await _menuService.getCurrentUserMenuList(framework); _state.patch((state) => state.menus = result.items); }