diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 8d14093..99259a5 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -45,5 +45,21 @@ android:name="android.permission.READ_EXTERNAL_STORAGE" /> - + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 2340daf..2d03c92 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -3,7 +3,7 @@ CADisableMinimumFrameDurationOnPhone - + CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName @@ -25,7 +25,7 @@ CFBundleVersion $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS - + Launch screen interface file base name $(LAUNCH_SCREEN_STORYBOARD) NSCameraUsageDescription @@ -35,13 +35,18 @@ NSPhotoLibraryUsageDescription App需要您的同意,才能访问相册 UIApplicationSupportsIndirectInputEvents - + + LSApplicationQueriesSchemes + + sms + tel + UILaunchStoryboardName LaunchScreen.storyboard UIMainStoryboardFile Main UIStatusBarHidden - + UISupportedInterfaceOrientations UIInterfaceOrientationPortrait @@ -56,6 +61,6 @@ UIInterfaceOrientationLandscapeRight UIViewControllerBasedStatusBarAppearance - + - + \ No newline at end of file diff --git a/lib/apis/index.dart b/lib/apis/index.dart index 7f8d2b3..e7e4b45 100644 --- a/lib/apis/index.dart +++ b/lib/apis/index.dart @@ -22,13 +22,18 @@ class Api { ); } -// 获取个人信息 - static Future getUserInfo() { +// 获取我的个人信息 + static Future getMyProfile() { return DioService.dio.get( Urls.userInfo, ); } +// 获取个人信息 + static Future getUserInfo(int userid) { + return DioService.dio.get('${Urls.userInfo}/$userid'); + } + // 分页获取项目列表 static Future> getProjects(Map params) { return DioService.dio.get(Urls.projects, diff --git a/lib/app_theme.dart b/lib/app_theme.dart index 07b7cbd..5413d6f 100644 --- a/lib/app_theme.dart +++ b/lib/app_theme.dart @@ -26,7 +26,7 @@ class AppTheme { static const Color dangerColor = Colors.red; static const Color dividerColor = Color.fromARGB(255, 224, 224, 224); static const Color appbarBgColor = AppTheme.primaryColor; - static const Color scaffoldBackgroundColor = Color(0xFFf5f8ff); + static const Color scaffoldBackgroundColor = Color(0XFFe9f0fd); static const Color inputFillColor = Color(0xFFf5f8ff); } diff --git a/lib/constants/global_url.dart b/lib/constants/global_url.dart index c9b6561..62704a0 100644 --- a/lib/constants/global_url.dart +++ b/lib/constants/global_url.dart @@ -5,7 +5,7 @@ class Urls { static String deleteAccount = 'user/deleteAccount'; static String saveUserInfo = 'user/saveUserInfo'; static String sysUser = 'system/users'; - static String userInfo = 'account/profile'; + static String myProfile = 'account/profile'; static String projects = 'project'; static String products = 'product'; static String inventoryInout = 'materials-in-out'; @@ -15,4 +15,5 @@ class Urls { static String uploadAttachemnt = 'tools/upload'; static String systemParamConfig = 'system/param-config'; static String accountMenus = 'account/menus'; + static String userInfo = 'system/users'; } diff --git a/lib/constants/router.dart b/lib/constants/router.dart index 9a5fc54..ae8675c 100644 --- a/lib/constants/router.dart +++ b/lib/constants/router.dart @@ -1,4 +1,6 @@ import 'package:get/get.dart'; +import 'package:sk_base_mobile/models/user_info.model.dart'; +import 'package:sk_base_mobile/screens/hr_manage/components/employee_detail.dart'; import 'package:sk_base_mobile/screens/hr_manage/hr_manage.dart'; import 'package:sk_base_mobile/screens/inventory/inventory.dart'; import 'package:sk_base_mobile/screens/login/login.dart'; @@ -14,6 +16,7 @@ class RouteConfig { static const String inventory = '/inventory'; static const String saleQuotation = '/sale_quotation'; static const String hrManage = '/hr_manage'; + static const String employeeDetail = '/employee_detail'; static final List getPages = [ GetPage(name: login, page: () => LoginScreen()), @@ -21,6 +24,7 @@ class RouteConfig { GetPage(name: userinfo, page: () => UserInfoPage()), GetPage(name: inventory, page: () => const InventoryPage()), GetPage(name: saleQuotation, page: () => SaleQuotationPage()), - GetPage(name: hrManage, page: () => HrManagePage()) + GetPage(name: hrManage, page: () => HrManagePage()), + GetPage(name: employeeDetail, page: () => EmployeeDetail()) ]; } diff --git a/lib/screens/hr_manage/components/dept_picker.dart b/lib/screens/hr_manage/components/dept_picker.dart new file mode 100644 index 0000000..8189131 --- /dev/null +++ b/lib/screens/hr_manage/components/dept_picker.dart @@ -0,0 +1,90 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:sk_base_mobile/app_theme.dart'; +import 'package:sk_base_mobile/util/screen_adaper_util.dart'; +import 'package:sk_base_mobile/widgets/core/sk_cascade_picker.dart'; +import 'package:sk_base_mobile/widgets/gradient_button.dart'; + +class DeptPicker extends StatelessWidget { + DeptPicker({super.key}); + + final _cascadeController = Get.put(CascadeController()); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration(color: AppTheme.nearlyWhite), + height: ScreenAdaper.height(400), + child: Column( + children: [ + GradientButton( + buttonText: '确定', + onPressed: () { + if (_cascadeController.isCompleted()) { + // 已选中的titles + List selectedTitles = + _cascadeController.selectedTitles; + print("已选中的titles: $selectedTitles"); + // 已选中的序号 + List selectedIndexes = + _cascadeController.selectedIndexes; + print("已选中的序号:$selectedIndexes"); + + Item item = _cascadeController + .items[selectedIndexes[0]] + .children![selectedIndexes[1]] + .children![selectedIndexes[2]]; + print("已选择item( ${item.name} )"); + } + }, + ), + SizedBox( + height: 10, + ), + Expanded( + child: CascadePicker( + initialPageData: + _cascadeController.items.map((e) => e.name!).toList(), + nextPageData: (pageCallback, currentPage, selectIndex) async { + print("当前选择: 第$currentPage页, 第$selectIndex项"); + if (currentPage == 1) { + // 在第一页选中,返回第二页列表数据 + List? nextPageData = _cascadeController + .items[selectIndex].children + ?.map((e) => e.name!) + .toList(); + if (nextPageData != null) pageCallback(nextPageData); + } else if (currentPage == 2) { + // 在第二页选中,返回第二页列表数据 + // 先获取已选中的序号 + List selectedIndexes = + _cascadeController.selectedIndexes; + // 根据已选中的序号在items中获取下一级页面的列表数据 + List? nextPageData = _cascadeController + .items[selectedIndexes[0]].children?[selectIndex].children + ?.map((e) => e.name!) + .toList(); + if (nextPageData != null) pageCallback(nextPageData); + } + }, + controller: _cascadeController, + maxPageNum: 3, + tabTitleStyle: TextStyle( + fontSize: ScreenAdaper.height(26), color: Colors.black), + itemTitleStyle: TextStyle( + fontSize: ScreenAdaper.height(26), color: Colors.black), + selectedIcon: + Icon(Icons.check, color: AppTheme.primaryColorLight), + )) + ], + )); + } +} + +class Item { + String? name; + String? code; + String? fatherCode; + String? remark; + List? children; +} diff --git a/lib/screens/hr_manage/components/edit_userinfo.dart b/lib/screens/hr_manage/components/edit_userinfo.dart new file mode 100644 index 0000000..10b0fa3 --- /dev/null +++ b/lib/screens/hr_manage/components/edit_userinfo.dart @@ -0,0 +1,285 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:sk_base_mobile/apis/index.dart'; +import 'package:sk_base_mobile/app_theme.dart'; +import 'package:sk_base_mobile/constants/bg_color.dart'; +import 'package:sk_base_mobile/models/user_info.model.dart'; +import 'package:sk_base_mobile/screens/hr_manage/components/dept_picker.dart'; +import 'package:sk_base_mobile/util/screen_adaper_util.dart'; +import 'package:sk_base_mobile/util/snack_bar.util.dart'; +import 'package:sk_base_mobile/widgets/core/sk_cascade_picker.dart'; +import 'package:sk_base_mobile/widgets/core/sk_dialog_header.dart'; +import 'package:sk_base_mobile/widgets/core/sk_text_input.dart'; +import 'package:sk_base_mobile/widgets/gradient_button.dart'; + +/// 编辑用户信息 +class EditUserInfo extends StatelessWidget { + final int userId; + final EditUserInfoController controller; + EditUserInfo({super.key, required this.userId}) + : controller = Get.put(EditUserInfoController(userId)); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + FocusScope.of(context).requestFocus(FocusNode()); + }, + child: buildBody(), + ); + } + + Widget buildBody() { + return Column( + children: [ + const SkDialogHeader(title: '编辑员工信息'), + Expanded( + child: SingleChildScrollView( + child: Container( + padding: EdgeInsets.symmetric( + horizontal: ScreenAdaper.height(15), + vertical: ScreenAdaper.height(15)), + child: Column(children: [ + buildAvatar(), + SizedBox( + height: ScreenAdaper.height(defaultPadding), + ), + SkTextInput( + isDense: true, + textController: controller.nameEditController, + labelText: '姓名', + ), + SizedBox( + height: ScreenAdaper.height(defaultPadding), + ), + SkTextInput( + isDense: true, + keyboardType: TextInputType.none, + textController: TextEditingController(), + labelText: '所属部门', + onTap: (_) async { + Get.bottomSheet(DeptPicker()) + .then((value) => Get.delete()); + }, + ), + SizedBox( + height: ScreenAdaper.height(defaultPadding), + ), + SkTextInput( + isDense: true, + keyboardType: TextInputType.none, + textController: TextEditingController(), + labelText: '角色', + onTap: (_) async { + Get.bottomSheet(Container( + height: ScreenAdaper.height(400), + decoration: BoxDecoration(color: AppTheme.nearlyWhite), + child: Column(children: [ + Container( + padding: EdgeInsets.symmetric( + vertical: ScreenAdaper.height(20), + horizontal: ScreenAdaper.width(20)), + child: Row( + children: [ + Text( + '取消', + style: TextStyle( + fontSize: ScreenAdaper.height(30)), + ), + Spacer(), + Text( + '确定', + style: TextStyle( + fontSize: ScreenAdaper.height(30), + color: AppTheme.primaryColor), + ) + ], + ), + ), + Expanded( + child: SingleChildScrollView( + child: ListBody( + children: [ + '角色1', + '角色2', + '角色3', + '角色4', + '角色5', + '角色6', + '角色7', + '角色8', + '角色9' + ].map((String text) { + return Obx( + () => CheckboxListTile( + contentPadding: EdgeInsets.symmetric( + vertical: ScreenAdaper.height(0), + horizontal: ScreenAdaper.width(20)), + title: Text(text), + value: + controller.selectedDepts.contains(text), + onChanged: (bool? value) { + if (value == true) { + controller.selectedDepts.add(text); + } else { + controller.selectedDepts.remove(text); + } + }, + ), + ); + }).toList(), + ), + )) + ]), + )); + }), + SizedBox( + height: ScreenAdaper.height(defaultPadding), + ), + SkTextInput( + isDense: true, + textController: TextEditingController(), + labelText: '登录用户名', + ), + SizedBox( + height: ScreenAdaper.height(defaultPadding), + ), + SkTextInput( + isDense: true, + textController: TextEditingController(), + labelText: '手机号', + ) + ]), + ), + )), + const GradientButton( + borderRadius: BorderRadius.zero, + buttonText: '提交', + ) + // Container( + // decoration: BoxDecoration(color: AppTheme.primaryColor), + // height: 100, + // ) + ], + ); + } + + Widget buildAvatar() { + return Column( + children: [ + SizedBox( + height: ScreenAdaper.height(10), + ), + buildImageUploader(), + ], + ); + } + + Widget builderImagePreview(String path, String type) { + return Stack( + children: [ + Container( + margin: EdgeInsets.symmetric(horizontal: ScreenAdaper.width(5)), + width: ScreenAdaper.width(180), + height: ScreenAdaper.width(180), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + border: Border.all(width: 1.0, color: AppTheme.dividerColor), + // color: AppTheme.primaryColor, + ), + child: Center( + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + image: DecorationImage( + fit: BoxFit.cover, + image: FileImage(File(path)), + ), + ), + ), + ), + ), + Positioned( + top: ScreenAdaper.height(5), + right: ScreenAdaper.width(5), + child: GestureDetector( + onTap: () { + if (type == 'agent') { + // controller.uploadAgentImgFilesPath.remove(path); + } else { + // controller.uploadProductImgFilesPath.remove(path); + } + }, + child: Icon( + Icons.close, + shadows: const [ + Shadow( + color: Colors.black, + offset: Offset(1, 1), + blurRadius: 3, + ), + Shadow( + color: Colors.black, + offset: Offset(-1, -1), + blurRadius: 3, + ), + ], + size: ScreenAdaper.height(40), + color: AppTheme.nearlyWhite, + ), + )) + ], + ); + } + + // 上传照片控制器 + Widget buildImageUploader() { + return GestureDetector( + onTap: () { + // controller.photoPicker(type); + }, + child: Container( + margin: EdgeInsets.symmetric(horizontal: ScreenAdaper.width(5)), + width: ScreenAdaper.width(180), + height: ScreenAdaper.width(180), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(100), + border: Border.all(width: 1.0, color: AppTheme.dividerColor), + // color: AppTheme.primaryColor, + ), + child: Center( + child: Icon( + Icons.add_a_photo_rounded, + size: ScreenAdaper.height(60), + color: AppTheme.primaryColor, + )))); + } +} + +class EditUserInfoController extends GetxController { + int userId; + EditUserInfoController(this.userId); + final nameEditController = TextEditingController(); + final userInfo = Rxn(); + RxList selectedDepts = RxList([]); + + @override + onReady() { + getUserInfo(); + super.onReady(); + } + + Future getUserInfo() async { + try { + final response = await Api.getUserInfo(userId); + if (response.data != null) { + userInfo.value = UserInfoModel.fromJson(response.data); + nameEditController.text = userInfo.value?.nickname ?? ''; + } + } catch (e) { + SnackBarUtil().error('$e'); + } finally {} + } +} diff --git a/lib/screens/hr_manage/components/employee_detail.dart b/lib/screens/hr_manage/components/employee_detail.dart new file mode 100644 index 0000000..6a6c92e --- /dev/null +++ b/lib/screens/hr_manage/components/employee_detail.dart @@ -0,0 +1,214 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; +import 'package:get/get.dart'; +import 'package:sk_base_mobile/app_theme.dart'; +import 'package:sk_base_mobile/config.dart'; +import 'package:sk_base_mobile/constants/bg_color.dart'; +import 'package:sk_base_mobile/models/user_info.model.dart'; +import 'package:sk_base_mobile/screens/inventory_inout/components/responsive.dart'; +import 'package:sk_base_mobile/util/screen_adaper_util.dart'; +import 'package:sk_base_mobile/widgets/core/sk_tag.dart'; +import 'package:sk_base_mobile/widgets/fade_in_cache_image.dart'; +import 'package:sk_base_mobile/widgets/core/sk_appbar.dart'; + +class EmployeeDetail extends StatelessWidget { + final _controller = Get.put(EmployeeDetailController()); + EmployeeDetail({super.key}); + final userInfo = Get.arguments as UserInfoModel; + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: const SkAppbar( + backgroundColor: AppTheme.white, + iconAndTextColor: AppTheme.black, + title: '员工详情', + ), + body: buildBody(), + ); + } + + Widget buildBody() { + return Column( + children: [ + buildTopInfo(), + Expanded(child: buildContent()), + ], + ); + } + + Widget buildTopInfo() { + return Container( + padding: EdgeInsets.symmetric( + vertical: ScreenAdaper.height(defaultPadding), + horizontal: ScreenAdaper.width(defaultPadding * 2)), + color: AppTheme.white, + child: Row( + children: [ + ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(30)), + child: FadeInCacheImage( + height: ScreenAdaper.height(80), + width: ScreenAdaper.height(80), + url: '${GloablConfig.OSS_URL}${userInfo.avatar}'), + ), + SizedBox( + width: ScreenAdaper.width(defaultPadding), + ), + // 中间信息 + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('${userInfo.nickname}', + style: TextStyle( + fontSize: ScreenAdaper.height(30), + fontWeight: FontWeight.w600)), + + SizedBox( + height: ScreenAdaper.height(5), + ), // role + Text('${userInfo.dept?.name}', + style: TextStyle(color: AppTheme.grey)), + SizedBox( + height: ScreenAdaper.height(5), + ), + Wrap( + spacing: ScreenAdaper.height(5), + runSpacing: ScreenAdaper.height(5), + children: [ + ...userInfo.roles.map((e) => SkTag( + text: '${e.name}', color: AppTheme.primaryColorLight)) + ], + ), + ], + )), + ], + ), + ); + } + + Widget buildContent() { + return Container( + margin: EdgeInsets.all(ScreenAdaper.height(defaultPadding)), + decoration: const BoxDecoration(color: AppTheme.white), + child: DefaultTabController( + length: 4, + initialIndex: _controller.selectedTabIndex.value, + child: Column( + children: [ + _buildTabBar(), + Expanded( + child: _buildTabView(), + ) + ], + )), + ); + } + + Widget _buildTabBar() { + return TabBar( + onTap: (index) { + _controller.selectedTabIndex.value = index; + }, + labelStyle: TextStyle( + fontSize: ScreenAdaper.height(25), fontWeight: FontWeight.w600), + labelPadding: EdgeInsets.zero, + labelColor: AppTheme.primaryColorLight, + // unselectedLabelColor: AppTheme.grey, + indicator: BoxDecoration( + border: Border( + bottom: BorderSide( + width: ScreenAdaper.height(5), + color: AppTheme.primaryColorLight, + )), + ), + tabs: const [ + Tab( + text: '基本信息', + ), + Tab( + text: '考勤记录', + ), + Tab( + text: '工作安排', + ), + Tab( + text: '相关文件', + ), + ], + ); + } + + Widget _buildTabView() { + return TabBarView( + children: [ + Responsive( + tablet: buildBaseInfo( + columnNum: 3, + ), + largeTablet: buildBaseInfo( + columnNum: 4, + ), + mobile: buildBaseInfo( + columnNum: 2, + )), + Container( + child: Text('考勤记录'), + ), + Container( + child: Text('请假记录'), + ), + Container( + child: Text('加班记录'), + ), + ], + ); + } + + //基本信息 + Widget buildBaseInfo({ + required int columnNum, + }) { + return Container( + padding: EdgeInsets.symmetric( + vertical: ScreenAdaper.height(defaultPadding), + horizontal: ScreenAdaper.height(defaultPadding)), + child: MasonryGridView.count( + crossAxisCount: columnNum, + itemCount: 7, + mainAxisSpacing: ScreenAdaper.height(50), + itemBuilder: (context, index) { + return buildBaseInfoItem(label: '姓名', value: '张三'); + }, + )); + } + + Widget buildBaseInfoItem({String? label, String? value}) { + return Container( + // decoration: BoxDecoration(color: Colors.red), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '$label', + style: TextStyle( + fontSize: ScreenAdaper.height(26), + color: AppTheme.grey, + fontWeight: FontWeight.w600), + ), + SizedBox( + height: ScreenAdaper.height(10), + ), + Text( + '$value', + style: TextStyle(fontSize: ScreenAdaper.height(26)), + ) + ], + ), + ); + } +} + +class EmployeeDetailController extends GetxController { + final selectedTabIndex = 0.obs; +} diff --git a/lib/screens/hr_manage/hr_manage.dart b/lib/screens/hr_manage/hr_manage.dart index d10871e..49d76d3 100644 --- a/lib/screens/hr_manage/hr_manage.dart +++ b/lib/screens/hr_manage/hr_manage.dart @@ -3,16 +3,26 @@ import 'package:get/get.dart'; import 'package:pull_to_refresh/pull_to_refresh.dart'; import 'package:sk_base_mobile/apis/index.dart'; import 'package:sk_base_mobile/app_theme.dart'; +import 'package:sk_base_mobile/config.dart'; import 'package:sk_base_mobile/constants/bg_color.dart'; +import 'package:sk_base_mobile/constants/constants.dart'; import 'package:sk_base_mobile/models/user_info.model.dart'; +import 'package:sk_base_mobile/screens/hr_manage/components/edit_userinfo.dart'; +import 'package:sk_base_mobile/util/common.util.dart'; import 'package:sk_base_mobile/util/date.util.dart'; import 'package:sk_base_mobile/util/debouncer.dart'; +import 'package:sk_base_mobile/util/device.util.dart'; +import 'package:sk_base_mobile/util/media_util.dart'; +import 'package:sk_base_mobile/util/modal.util.dart'; import 'package:sk_base_mobile/util/screen_adaper_util.dart'; +import 'package:sk_base_mobile/util/snack_bar.util.dart'; import 'package:sk_base_mobile/widgets/core/sk_ink.dart'; import 'package:sk_base_mobile/widgets/core/sk_tag.dart'; import 'package:sk_base_mobile/widgets/core/sk_text_input.dart'; import 'package:sk_base_mobile/widgets/empty.dart'; -import 'package:sk_base_mobile/widgets/sk_appbar.dart'; +import 'package:sk_base_mobile/widgets/fade_in_cache_image.dart'; +import 'package:sk_base_mobile/widgets/loading_indicator.dart'; +import 'package:sk_base_mobile/widgets/core/sk_appbar.dart'; class HrManagePage extends StatelessWidget { final controller = Get.put(HrManageController()); @@ -20,14 +30,21 @@ class HrManagePage extends StatelessWidget { @override Widget build(BuildContext context) { + // return Column( + // children: [ + // Expanded( + // child: Material( + // child: EditUserInfo(), + // )) + // ], + // ); return GestureDetector( onTap: () { // 取消焦点 FocusScope.of(context).requestFocus(FocusNode()); }, child: Scaffold( - backgroundColor: Color(0xFFe9f0fd), - appBar: SkAppbar( + appBar: const SkAppbar( backgroundColor: AppTheme.nearlyWhite, iconAndTextColor: AppTheme.black, title: '人事管理', @@ -43,25 +60,30 @@ class HrManagePage extends StatelessWidget { Expanded( child: Container( margin: EdgeInsets.symmetric( - horizontal: ScreenAdaper.height(defaultPadding), - vertical: ScreenAdaper.height(defaultPadding)), + horizontal: ScreenAdaper.height(defaultPadding), + ), child: Obx( - () => SmartRefresher( - enablePullDown: true, - enablePullUp: true, - controller: controller.refreshController, - onLoading: controller.onLoading, - onRefresh: controller.onRefresh, - child: controller.list.isEmpty - ? const Center( - child: Empty(text: '暂无数据'), - ) - : ListView.builder( - itemBuilder: (context, index) { - return buildUserCard(index); - }, - itemCount: controller.list.length, - )), + () => controller.loading.value + ? const LoadingIndicator(common: true) + : SmartRefresher( + enablePullDown: true, + enablePullUp: true, + controller: controller.refreshController, + onLoading: controller.onLoading, + onRefresh: controller.onRefresh, + child: controller.list.isEmpty + ? const Center( + child: Empty(text: '暂无员工'), + ) + : ListView.builder( + padding: EdgeInsets.symmetric( + vertical: + ScreenAdaper.height(defaultPadding)), + itemBuilder: (context, index) { + return buildUserCard(index); + }, + itemCount: controller.list.length, + )), )), ) ], @@ -75,11 +97,8 @@ class HrManagePage extends StatelessWidget { }, delayTime: 500); return Container( color: AppTheme.nearlyWhite, - padding: EdgeInsets.only( - right: ScreenAdaper.width(20), - left: ScreenAdaper.width(20), - bottom: ScreenAdaper.height(20)), - child: Row(children: [ + padding: EdgeInsets.all(ScreenAdaper.width(20)), + child: Row(crossAxisAlignment: CrossAxisAlignment.center, children: [ Expanded( child: SizedBox( height: ScreenAdaper.height(70), @@ -132,98 +151,124 @@ class HrManagePage extends StatelessWidget { } Widget buildUserCard(int index) { - return Container( - padding: EdgeInsets.symmetric( - horizontal: ScreenAdaper.height(defaultPadding), - vertical: ScreenAdaper.height(defaultPadding)), + return SkInk( + onTap: () { + Get.toNamed(RouteConfig.employeeDetail, + arguments: controller.list[index]); + }, margin: EdgeInsets.only(bottom: ScreenAdaper.height(defaultPadding)), - decoration: BoxDecoration( - color: AppTheme.white, - borderRadius: BorderRadius.circular(ScreenAdaper.sp(15))), - child: Column( - children: [ - Row( - children: [ - // 头像 - ClipRRect( - borderRadius: BorderRadius.all(Radius.circular(30)), - child: Image( - height: ScreenAdaper.height(80), - image: NetworkImage( - 'https://thirdqq.qlogo.cn/g?b=qq&s=100&nk=1743369777')), - ), - SizedBox( - width: ScreenAdaper.height(defaultPadding), - ), - // 中间信息 - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('${controller.list[index].nickname}', - style: TextStyle( - fontSize: ScreenAdaper.height(30), - fontWeight: FontWeight.w600)), + borderRadius: BorderRadius.circular(ScreenAdaper.sp(15)), + child: Container( + padding: EdgeInsets.symmetric( + horizontal: ScreenAdaper.height(defaultPadding), + vertical: ScreenAdaper.height(defaultPadding)), + child: Column( + children: [ + Row( + children: [ + // 头像 + ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(30)), + child: FadeInCacheImage( + height: ScreenAdaper.height(80), + width: ScreenAdaper.height(80), + url: + MediaUtil.getMediaUrl(controller.list[index].avatar)), + ), + SizedBox( + width: ScreenAdaper.height(defaultPadding), + ), + // 中间信息 + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('${controller.list[index].nickname}', + style: TextStyle( + fontSize: ScreenAdaper.height(30), + fontWeight: FontWeight.w600)), - SizedBox( - height: ScreenAdaper.height(5), - ), // role - Text('${controller.list[index].dept?.name}', - style: TextStyle(color: AppTheme.grey)), - SizedBox( - height: ScreenAdaper.height(5), - ), - Container( - child: Wrap( - spacing: ScreenAdaper.height(5), - runSpacing: ScreenAdaper.height(5), - children: [ - ...controller.list[index].roles.map((e) => SkTag( - text: '${e.name}', color: AppTheme.primaryColorLight)) - ], - )), - ], - )), + SizedBox( + height: ScreenAdaper.height(5), + ), // role + Text('${controller.list[index].dept?.name}', + style: TextStyle(color: AppTheme.grey)), + SizedBox( + height: ScreenAdaper.height(5), + ), + Container( + child: Wrap( + spacing: ScreenAdaper.height(5), + runSpacing: ScreenAdaper.height(5), + children: [ + ...controller.list[index].roles.map((e) => SkTag( + text: '${e.name}', + color: AppTheme.primaryColorLight)) + ], + )), + ], + )), - /// 右侧action - /// 电话 - buildActionButton( - onTap: () {}, - icon: Icons.phone, - ), - SizedBox( - width: ScreenAdaper.height(defaultPadding), - ), + /// 右侧action + /// 电话 + buildActionButton( + onTap: () { + if (controller.list[index].phone == null) { + SnackBarUtil().info('该员工没有录入手机号'); + return; + } + DeviceUtil.callPhone(controller.list[index].phone!); + }, + icon: Icons.phone, + ), + SizedBox( + width: ScreenAdaper.height(defaultPadding), + ), - /// 邮件 - buildActionButton( - onTap: () {}, - icon: Icons.email_outlined, - ), - SizedBox( - width: ScreenAdaper.height(defaultPadding), - ), + /// 邮件 + buildActionButton( + onTap: () { + if (controller.list[index].email == null) { + SnackBarUtil().info('该员工没有录入邮箱'); + return; + } + DeviceUtil.sendEmail(controller.list[index].email!); + }, + icon: Icons.email_outlined, + ), + SizedBox( + width: ScreenAdaper.height(defaultPadding), + ), - /// 编辑 - buildActionButton( - onTap: () {}, - icon: Icons.edit, - ) - ], - ), - const Divider(), - Row( - children: [ - Text( - '工龄: ${SkDateUtil.howLongAgo(controller.list[index].createdAt!)}'), - const Spacer(), - const SkTag( - text: '在职', - color: Colors.green, - ) - ], - ) - ], + /// 编辑 + buildActionButton( + onTap: () { + ModalUtil.showGeneralDialog( + content: EditUserInfo( + userId: controller.list[index].id!, + )).then((value) => Get.delete()); + + // Get.toNamed(RouteConfig.employeeDetail, + // arguments: controller.list[index]); + }, + icon: Icons.edit, + ) + ], + ), + const Divider(), + Row( + children: [ + Text( + '工龄: ${SkDateUtil.howLongAgo(controller.list[index].createdAt!)}'), + const Spacer(), + const SkTag( + text: '在职', + color: Colors.green, + ) + ], + ) + ], + ), ), ); } @@ -233,14 +278,14 @@ class HrManagePage extends StatelessWidget { required IconData icon, }) { return SkInk( - onTap: () {}, + onTap: onTap, border: Border.all(color: AppTheme.grey.withOpacity(0.8)), - borderRadius: BorderRadius.circular(ScreenAdaper.sp(30)), + borderRadius: BorderRadius.circular(ScreenAdaper.sp(40)), child: Container( padding: EdgeInsets.all(ScreenAdaper.height(5)), child: Icon( icon, - size: ScreenAdaper.height(35), + size: ScreenAdaper.height(40), ), ), ); @@ -251,10 +296,27 @@ class HrManageController extends GetxController { RxList list = RxList([]); RxString searchKey = ''.obs; final searchBarTextConroller = TextEditingController(); - RefreshController refreshController = RefreshController(initialRefresh: true); + RefreshController refreshController = + RefreshController(initialRefresh: false); int page = 1; int limit = 15; int total = 0; + RxBool loading = false.obs; + @override + onReady() { + super.onReady(); + initData(); + } + + initData() async { + loading.value = true; + try { + await getData(isRefresh: true); + } finally { + loading.value = false; + } + } + Future> getData({bool isRefresh = false}) async { if (isRefresh == true) { page = 1; diff --git a/lib/screens/inventory/inventory.dart b/lib/screens/inventory/inventory.dart index d5aca44..1845824 100644 --- a/lib/screens/inventory/inventory.dart +++ b/lib/screens/inventory/inventory.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:sk_base_mobile/screens/new_inventory_inout/components/inventory_search.dart'; -import 'package:sk_base_mobile/widgets/sk_appbar.dart'; +import 'package:sk_base_mobile/widgets/core/sk_appbar.dart'; class InventoryPage extends StatelessWidget { final bool isPage; diff --git a/lib/screens/inventory_inout/components/inventory_inout_info.dart b/lib/screens/inventory_inout/components/inventory_inout_info.dart index f47d6fc..2af6374 100644 --- a/lib/screens/inventory_inout/components/inventory_inout_info.dart +++ b/lib/screens/inventory_inout/components/inventory_inout_info.dart @@ -11,6 +11,7 @@ import 'package:sk_base_mobile/util/screen_adaper_util.dart'; import 'package:sk_base_mobile/widgets/fade_in_cache_image.dart'; import 'package:sk_base_mobile/widgets/image_preview.dart'; import 'package:sk_base_mobile/widgets/loading_indicator.dart'; +import 'package:sk_base_mobile/widgets/core/sk_dialog_header.dart'; class InventoryInoutInfo extends StatelessWidget { final int inventoryInoutId; @@ -22,23 +23,7 @@ class InventoryInoutInfo extends StatelessWidget { Widget build(BuildContext context) { return SizedBox( child: Column(children: [ - Container( - padding: EdgeInsets.symmetric( - horizontal: ScreenAdaper.width(10), - vertical: ScreenAdaper.height(15)), - decoration: const BoxDecoration(color: AppTheme.primaryColor), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - '出入库详情', - style: TextStyle( - fontSize: ScreenAdaper.height(25), - color: AppTheme.nearlyWhite), - ), - ], - ), - ), + const SkDialogHeader(title: '出入库详情'), Expanded( child: Obx(() => controller.inventoryInoutInfo.value == null ? const Center(child: LoadingIndicator(common: true)) diff --git a/lib/screens/mine/mine.controller.dart b/lib/screens/mine/mine.controller.dart index 438920e..98b62bf 100644 --- a/lib/screens/mine/mine.controller.dart +++ b/lib/screens/mine/mine.controller.dart @@ -15,7 +15,7 @@ class MineController extends GetxController { @override void onInit() { - // AuthStore.to.getUserInfo(); + // AuthStore.to.getMyProfile(); super.onInit(); } diff --git a/lib/screens/new_inventory_inout/new_inventory_inout.dart b/lib/screens/new_inventory_inout/new_inventory_inout.dart index 0110f9c..bbd1050 100644 --- a/lib/screens/new_inventory_inout/new_inventory_inout.dart +++ b/lib/screens/new_inventory_inout/new_inventory_inout.dart @@ -19,7 +19,7 @@ import 'package:sk_base_mobile/widgets/core/sk_date_picker.dart'; import 'package:sk_base_mobile/widgets/core/sk_text_input.dart'; import 'package:sk_base_mobile/widgets/gradient_button.dart'; import 'package:sk_base_mobile/screens/new_inventory_inout/new_inventory_inout_controller.dart'; -import 'package:sk_base_mobile/widgets/sk_appbar.dart'; +import 'package:sk_base_mobile/widgets/core/sk_appbar.dart'; class NewInventoryInout extends StatelessWidget { final NewInventoryInoutController controller; diff --git a/lib/screens/sale_quotation/components/sale_quotation_drawer.dart b/lib/screens/sale_quotation/components/sale_quotation_drawer.dart index 53e599f..c70cd9e 100644 --- a/lib/screens/sale_quotation/components/sale_quotation_drawer.dart +++ b/lib/screens/sale_quotation/components/sale_quotation_drawer.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:sk_base_mobile/app_theme.dart'; import 'package:sk_base_mobile/util/screen_adaper_util.dart'; import 'package:sk_base_mobile/widgets/gradient_button.dart'; -import 'package:sk_base_mobile/widgets/sk_appbar.dart'; +import 'package:sk_base_mobile/widgets/core/sk_appbar.dart'; class SaleQuotationEndDrawer extends StatelessWidget { const SaleQuotationEndDrawer({super.key}); diff --git a/lib/screens/sale_quotation/sale_quotation.dart b/lib/screens/sale_quotation/sale_quotation.dart index 1d6013e..b396bc4 100644 --- a/lib/screens/sale_quotation/sale_quotation.dart +++ b/lib/screens/sale_quotation/sale_quotation.dart @@ -1,5 +1,3 @@ -import 'dart:ffi'; - import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; @@ -14,10 +12,9 @@ import 'package:sk_base_mobile/util/snack_bar.util.dart'; import 'package:sk_base_mobile/widgets/core/sk_number_input.dart'; import 'package:sk_base_mobile/widgets/core/sk_text_input.dart'; import 'package:sk_base_mobile/widgets/empty.dart'; -import 'package:sk_base_mobile/widgets/sk_appbar.dart'; +import 'package:sk_base_mobile/widgets/core/sk_appbar.dart'; import 'package:flutter_sticky_header/flutter_sticky_header.dart'; import 'package:math_expressions/math_expressions.dart'; -import 'dart:math' as math; class SaleQuotationPage extends StatelessWidget { SaleQuotationPage({super.key}); diff --git a/lib/screens/workbench/workbench.dart b/lib/screens/workbench/workbench.dart index 9411965..ca97c52 100644 --- a/lib/screens/workbench/workbench.dart +++ b/lib/screens/workbench/workbench.dart @@ -7,7 +7,7 @@ import 'package:sk_base_mobile/constants/router.dart'; import 'package:sk_base_mobile/screens/workbench/workbench_controller.dart'; import 'package:sk_base_mobile/util/screen_adaper_util.dart'; import 'package:sk_base_mobile/util/snack_bar.util.dart'; -import 'package:sk_base_mobile/widgets/sk_appbar.dart'; +import 'package:sk_base_mobile/widgets/core/sk_appbar.dart'; class WorkBenchPage extends StatelessWidget { WorkBenchPage({super.key}); diff --git a/lib/screens/workbench/workbench_controller.dart b/lib/screens/workbench/workbench_controller.dart index f0562c9..1072994 100644 --- a/lib/screens/workbench/workbench_controller.dart +++ b/lib/screens/workbench/workbench_controller.dart @@ -1,4 +1,3 @@ -import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:sk_base_mobile/models/workbench.model.dart'; diff --git a/lib/store/auth.store.dart b/lib/store/auth.store.dart index 812cb7c..ddda41b 100644 --- a/lib/store/auth.store.dart +++ b/lib/store/auth.store.dart @@ -26,7 +26,7 @@ class AuthStore extends GetxService { if (token != null) { if (preUserInfo != null) { - await getUserInfo(); + await getMyProfile(); LoggerUtil().info('[Store-Auth] userId: ${userInfo.value.id}'); } } @@ -87,7 +87,7 @@ class AuthStore extends GetxService { await StorageService.to .setString(CacheKeys.token, auth.token!, isWithUser: false); } - await getUserInfo(); + await getMyProfile(); await getCommonInfo(); Get.offNamed(RouteConfig.home); } @@ -105,9 +105,9 @@ class AuthStore extends GetxService { .setString(CacheKeys.userInfo, jsonEncode(newInfo), isWithUser: false); } - Future getUserInfo() async { + Future getMyProfile() async { try { - final response = await Api.getUserInfo(); + final response = await Api.getMyProfile(); if (response.data != null) { UserInfoModel userInfo = UserInfoModel.fromJson(response.data); await updateUserInfoState(userInfo); @@ -122,7 +122,7 @@ class AuthStore extends GetxService { try { final res = await Api.saveUserInfo(data); if (res.data != null) { - await getUserInfo(); + await getMyProfile(); SnackBarUtil().success('Save successfully.'); } } catch (e) { diff --git a/lib/util/device.util.dart b/lib/util/device.util.dart index 5022370..4fdb858 100644 --- a/lib/util/device.util.dart +++ b/lib/util/device.util.dart @@ -1,7 +1,30 @@ import 'dart:io'; import 'package:package_info/package_info.dart'; +import 'package:url_launcher/url_launcher.dart'; class DeviceUtil { + ///发邮件 + static void sendEmail(String email) { + if (email.isNotEmpty) { + String url = 'mailto:$email'; + launchUrl(Uri.parse(url)); + } + } + + ///打电话 + static void callPhone(String phone) { + if (phone.isNotEmpty) { + String url = 'tel:$phone'; + if (Platform.isIOS) { + url = 'tel://$phone'; + } + if (Platform.isAndroid) { + url = 'tel:$phone'; + } + launchUrl(Uri.parse(url)); + } + } + /// 获取当前版本 static Future getAppVersion() async { PackageInfo packageInfo = await PackageInfo.fromPlatform(); diff --git a/lib/util/media_util.dart b/lib/util/media_util.dart index 0c19924..5128e2e 100644 --- a/lib/util/media_util.dart +++ b/lib/util/media_util.dart @@ -5,10 +5,19 @@ import 'package:dio/dio.dart'; import 'package:image_gallery_saver/image_gallery_saver.dart'; import 'package:sk_base_mobile/apis/index.dart'; import 'package:image_picker/image_picker.dart'; +import 'package:sk_base_mobile/config.dart'; import 'package:sk_base_mobile/models/upload_result.model.dart'; import 'package:sk_base_mobile/services/service.dart'; class MediaUtil { + static String? getMediaUrl(String? url) { + if ((url ?? '').isEmpty) { + return null; + } else { + return '${GloablConfig.OSS_URL}$url'; + } + } + //拍照 Future getImageFromCamera( {double? maxWidth, bool isFront = false}) async { @@ -52,6 +61,15 @@ class MediaUtil { return null; } + // 保存到相册 + Future saveImageToPhotoLib({required String imgUrl, String? filename}) async { + var response = await DioService.dio + .get(imgUrl, options: Options(responseType: ResponseType.bytes)); + final result = await ImageGallerySaver.saveImage( + Uint8List.fromList(response.data), + quality: 60); + return result; + } // // 上传一张照片 // Future uploadMediaWithProgress( // File imgfile, Function onProgress) async { @@ -76,16 +94,6 @@ class MediaUtil { // } // } - // 保存到相册 - Future saveImageToPhotoLib({required String imgUrl, String? filename}) async { - var response = await DioService.dio - .get(imgUrl, options: Options(responseType: ResponseType.bytes)); - final result = await ImageGallerySaver.saveImage( - Uint8List.fromList(response.data), - quality: 60); - return result; - } - // ///选取视频 // Future getVideoFromGallery() async { // if (AppInfoModel().isCameraing) { diff --git a/lib/util/modal.util.dart b/lib/util/modal.util.dart index 768e5ae..4db115b 100644 --- a/lib/util/modal.util.dart +++ b/lib/util/modal.util.dart @@ -167,7 +167,7 @@ class ModalUtil { double? height, Offset? offset}) { return Get.generalDialog( - barrierLabel: "productPicker", + barrierLabel: "generalDialog", barrierDismissible: true, transitionDuration: const Duration(milliseconds: 400), pageBuilder: (_, __, ___) { diff --git a/lib/util/snack_bar.util.dart b/lib/util/snack_bar.util.dart index 689d00f..1b958bb 100644 --- a/lib/util/snack_bar.util.dart +++ b/lib/util/snack_bar.util.dart @@ -74,7 +74,7 @@ class SnackBarUtil { borderRadius: 15, margin: EdgeInsets.symmetric( horizontal: ScreenAdaper.height(15), vertical: 0), - duration: const Duration(seconds: 1), + duration: const Duration(seconds: 2), dismissDirection: DismissDirection.horizontal, forwardAnimationCurve: Curves.fastLinearToSlowEaseIn, reverseAnimationCurve: Curves.linearToEaseOut); diff --git a/lib/widgets/sk_appbar.dart b/lib/widgets/core/sk_appbar.dart similarity index 96% rename from lib/widgets/sk_appbar.dart rename to lib/widgets/core/sk_appbar.dart index a931dc9..380f95f 100644 --- a/lib/widgets/sk_appbar.dart +++ b/lib/widgets/core/sk_appbar.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:sk_base_mobile/app_theme.dart'; import 'package:sk_base_mobile/util/util.dart'; class SkAppbar extends StatelessWidget implements PreferredSizeWidget { diff --git a/lib/widgets/core/sk_cascade_picker.dart b/lib/widgets/core/sk_cascade_picker.dart new file mode 100644 index 0000000..d7f30a7 --- /dev/null +++ b/lib/widgets/core/sk_cascade_picker.dart @@ -0,0 +1,468 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:sk_base_mobile/app_theme.dart'; +import 'package:sk_base_mobile/screens/hr_manage/components/dept_picker.dart'; + +/// 级联选择器 +/// 使用示例: +/// ```dart +/// CascadePicker的page是ListView,没有约束的情况下它的高度是无限的, +/// 因此需要约束高度。 +/// +/// final _cascadeController = CascadeController(); +/// +/// initialPageData: 第一页的数据 +/// nextPageData: 下一页的数据,点击当前页的选择项后调用该方法加载下一页 +/// - pageCallback: 用于传递下一页的数据给CascadePicker +/// - currentPage: 当前是第几页 +/// - selectIndex: 当前选中第几项 +/// controller: 控制器,用于获取已选择的数据 +/// maxPageNum: 最大页数 +/// selectedIcon: 已选中选项前面的图标,flutter package不能放本地资源文件,因此需要从外部传入,图标在images文件夹下面 +/// +/// Expand( +/// child: CascadePicker( +/// initialPageData: ['a', 'b', 'c', 'd'], +/// nextPageData: (pageCallback, currentPage, selectIndex) async { +/// pageCallback(['one', 'two', 'three']) +/// }, +/// controller: _cascadeController, +/// maxPageNum: 4, +/// selectedIcon: Image.asset("images/ic_select_mark.png", width: 10, height: 10, color: Colors.redAccent,), +/// ) +/// +/// InkBox( +/// child: Container(...) +/// onTap: () { +/// /// 判断是否完成选择 +/// if (_cascadeController.isCompleted()) { +/// List selectedTitles = _cascadeController.selectedTitles; +/// List selectedIndexes = _cascadeController.selectedIndexes; +/// } +/// } +/// ) +/// ``` + +/// pageData: 下一页的数据 +/// currentPage: 当前是第几页, +/// selectIndex: 当前页选中第几项 +typedef void NextPageCallback( + Function(List) pageData, int currentPage, int selectIndex); + +class CascadePicker extends StatefulWidget { + final List initialPageData; + final NextPageCallback nextPageData; + final int maxPageNum; + final CascadeController controller; + final Color tabColor; + final double tabHeight; + final TextStyle tabTitleStyle; + final double itemHeight; + final TextStyle itemTitleStyle; + final Color itemColor; + final Color activeColor; + final Widget? selectedIcon; + + CascadePicker( + {required this.initialPageData, + required this.nextPageData, + this.maxPageNum = 3, + required this.controller, + this.tabHeight = 40, + this.activeColor = AppTheme.primaryColor, + this.tabColor = Colors.white, + this.tabTitleStyle = const TextStyle(color: Colors.black, fontSize: 14), + this.itemHeight = 40, + this.itemColor = Colors.white, + this.itemTitleStyle = const TextStyle(color: Colors.black, fontSize: 14), + this.selectedIcon}); + + @override + _CascadePickerState createState() => _CascadePickerState(this.controller); +} + +class _CascadePickerState extends State + with SingleTickerProviderStateMixin { + static String _newTabName = "请选择"; + + final CascadeController _cascadeController; + + _CascadePickerState(this._cascadeController) { + _cascadeController._setState(this); + } + + late final AnimationController _controller; + late final CurvedAnimation _curvedAnimation; + Animation? _sliderAnimation; + final _sliderFixMargin = ValueNotifier(0.0); + double _sliderWidth = 20; + + PageController _pageController = PageController(initialPage: 0); + + GlobalKey _sliderKey = GlobalKey(); + List _tabKeys = []; + + /// 选择器数据集合 + List> _pagesData = []; + + /// 已选择的title集合 + List _selectedTabs = [_newTabName]; + + /// 已选择的item index集合 + List _selectedIndexes = [-1]; + + /// "请选择"tab宽度,添加新的tab时用到 + double _animTabWidth = 0; + + /// tab添加事件记录,用于隐藏"请选择"tab初始化状态 + bool _isAddTabEvent = false; + + /// tab移动未开始,渲染'请选择'tab时隐藏文本,这时的tab在终点位置 + bool _isAnimateTextHide = false; + + /// 防止_moveSlider重复调用 + bool _isClickAndMoveTab = false; + + /// 当前选择的页面,移动滑块前赋值 + int _currentSelectPage = 0; + + _addTab(int page, int atIndex, String currentPageItem) { + _loadNextPageData(page, atIndex, currentPageItem); + } + + _loadNextPageData(int page, int atIndex, String currentPageItem, + {bool isUpdatePage = false}) { + widget.nextPageData((data) { + final nextPageDataIsEmpty = data.isEmpty; + if (!nextPageDataIsEmpty) { + /// 下一页有数据,更新本页数据或添加新的页面 + setState(() { + if (isUpdatePage) { + /// 更新下一页 + _pagesData[page] = data; + _selectedTabs[page] = _newTabName; + _selectedIndexes[page] = -1; + + /// 清空下下页以后的所有页面和tab数据 + _pagesData.removeRange(page + 1, _pagesData.length); + _selectedIndexes.removeRange(page + 1, _selectedIndexes.length); + _selectedTabs.removeRange(page + 1, _selectedTabs.length); + } else { + /// 添加新的页面 + _isAnimateTextHide = true; + _isAddTabEvent = true; + _pagesData.add(data); + _selectedTabs.add(_newTabName); + _selectedIndexes.add(-1); + } + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + _moveSlider(page, isAdd: true); + }); + }); + } else { + /// 如果下一页数据为空,那么更新本页数据 + final currentPage = page - 1; + setState(() { + _selectedTabs[currentPage] = currentPageItem; + _selectedIndexes[currentPage] = atIndex; + + /// 下一页数据为空,清空下一页以后的所有页面和tab数据 + _pagesData.removeRange(page, _pagesData.length); + _selectedIndexes.removeRange(page, _selectedIndexes.length); + _selectedTabs.removeRange(page, _selectedTabs.length); + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + // 调整滑块位置 + _moveSlider(currentPage); + }); + }); + } + }, page, atIndex); + } + + _moveSlider(int page, {bool movePage = true, bool isAdd = false}) { + if (movePage && _currentSelectPage != page) { + /// 上一次选择的页面和本次选择的页面不同时,移动tab标签, + /// 移动时先把_isClickAndMoveTab设为true,防止滑动PageView + /// 时_moveSlider重复调用。 + _isClickAndMoveTab = true; + } + _isAddTabEvent = isAdd; + _currentSelectPage = page; + + if (_controller.isAnimating) { + _controller.stop(); + } + RenderBox slider = + _sliderKey.currentContext?.findRenderObject() as RenderBox; + Offset sliderPosition = slider.localToGlobal(Offset.zero); + RenderBox currentTabBox = + _tabKeys[page].currentContext?.findRenderObject() as RenderBox; + Offset currentTabPosition = currentTabBox.localToGlobal(Offset.zero); + + _animTabWidth = currentTabBox.size.width; + + final begin = sliderPosition.dx - _sliderFixMargin.value; + final end = currentTabPosition.dx + + (currentTabBox.size.width - _sliderWidth) / 2 - + _sliderFixMargin.value; + _sliderAnimation = + Tween(begin: begin, end: end).animate(_curvedAnimation); + _controller.value = 0; + _controller.forward(); + if (movePage) { + _pageController.animateToPage(page, + curve: Curves.linear, duration: Duration(milliseconds: 500)); + } + } + + /// 注意:tab渲染完成才开始动画,即调用moveSlider,这个方法会在动画执行期间多次调用 + Widget _animateTab({required Widget tab}) { + return Transform.translate( + offset: Offset( + Tween(begin: _isAddTabEvent ? -_animTabWidth : 0, end: 0) + .evaluate(_curvedAnimation), + 0), + child: Opacity( + + /// 动画未开始前隐藏文本 + opacity: _isAnimateTextHide ? 0 : 1, + child: tab), + ); + } + + List _tabWidgets() { + List widgets = []; + _tabKeys.clear(); + for (int i = 0; i < _pagesData.length; i++) { + GlobalKey key = GlobalKey(); + _tabKeys.add(key); + final tab = GestureDetector( + child: Container( + key: key, + height: widget.tabHeight, + color: widget.tabColor, + alignment: Alignment.center, + padding: EdgeInsets.symmetric(horizontal: 15), + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: + MediaQuery.of(context).size.width / _pagesData.length - 10), + child: Text( + _selectedTabs[i], + style: _currentSelectPage == i + ? widget.tabTitleStyle.copyWith(color: widget.activeColor) + : widget.tabTitleStyle, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ), + onTap: () { + _moveSlider(i); + }, + ); + if (i == _pagesData.length - 1 && _selectedTabs[i] == _newTabName) { + widgets.add(_animateTab(tab: tab)); + _isAnimateTextHide = false; + } else { + widgets.add(tab); + } + } + return widgets; + } + + /// 选择项 + Widget _pageItemWidget(int index, int page, String item) { + return GestureDetector( + child: Container( + alignment: Alignment.centerLeft, + padding: EdgeInsets.symmetric(horizontal: 15), + height: widget.itemHeight, + color: widget.itemColor, + child: Row( + children: [ + item == _selectedTabs[page] + ? Padding( + padding: const EdgeInsets.all(5.0), + child: widget.selectedIcon == null + ? Icon(Icons.chevron_right, + size: 15, color: widget.activeColor) + : widget.selectedIcon, + ) + : SizedBox(), + Text("$item", + style: item == _selectedTabs[page] + ? widget.itemTitleStyle.copyWith(color: widget.activeColor) + : widget.itemTitleStyle), + ], + ), + ), + onTap: () { + if (page == widget.maxPageNum - 1) { + /// 当前页是最后一页 + setState(() { + _selectedTabs[page] = item; + _selectedIndexes[page] = index; + + /// 调整滑块位置 + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + _moveSlider(page); + }); + }); + } else if (_tabKeys.length >= widget.maxPageNum || + page < _tabKeys.length - 1) { + if (index == _selectedIndexes[page]) { + /// 选择相同的item + _moveSlider(page + 1); + } else { + /// 选择不同的item,更新tab renderBox + setState(() { + _selectedTabs[page] = item; + _selectedIndexes[page] = index; +// _selectedIndexes.removeRange(page + 1, _selectedIndexes.length); + }); + _loadNextPageData(page + 1, index, item, isUpdatePage: true); + } + } else { + /// 添加新tab页面 + /// page == _tabKeys.length - 1 && _tabKeys.length == widget.maxPageNum + _selectedTabs[page] = item; + _selectedIndexes[page] = index; + _addTab(page + 1, index, item); + } + }, + ); + } + + Widget _pageWidget(int page) { + return ListView.builder( + padding: EdgeInsets.zero, + itemCount: _pagesData[page].length, + itemBuilder: (context, index) => + _pageItemWidget(index, page, _pagesData[page][index]), +// separatorBuilder: (context, index) => Divider(height: 0.3, thickness: 0.3, color: Color(0xffdddddd), indent: 15, endIndent: 15,), + ); + } + + @override + void initState() { + super.initState(); + _pagesData.add(widget.initialPageData); + + _controller = AnimationController( + duration: const Duration(milliseconds: 500), vsync: this); + + _curvedAnimation = CurvedAnimation(parent: _controller, curve: Curves.ease) + ..addStatusListener((state) {}); + + _sliderAnimation = + Tween(begin: 0, end: 10).animate(_curvedAnimation); + + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + RenderBox tabBox = + _tabKeys.first.currentContext?.findRenderObject() as RenderBox; + _sliderFixMargin.value = (tabBox.size.width - _sliderWidth) / 2; + }); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AnimatedBuilder( + animation: _sliderAnimation!, + builder: (context, child) => Stack( + clipBehavior: Clip.hardEdge, + alignment: Alignment.bottomLeft, + children: [ + Container( + width: MediaQuery.of(context).size.width, + child: Row( + children: _tabWidgets(), + ), + ), + ValueListenableBuilder( + valueListenable: _sliderFixMargin, + builder: (_, margin, __) => Positioned( + left: margin + _sliderAnimation!.value, + child: Container( + key: _sliderKey, + width: _sliderWidth, + height: 2, + decoration: BoxDecoration( + color: widget.activeColor, + borderRadius: BorderRadius.circular(2)), + ), + ), + ) + ], + ), + ), + Expanded( + child: PageView.builder( + itemCount: _pagesData.length, + controller: _pageController, + itemBuilder: (context, index) => _pageWidget(index), + onPageChanged: (position) { + if (!_isClickAndMoveTab) { + _moveSlider(position, movePage: false); + } + if (_currentSelectPage == position) { + _isClickAndMoveTab = false; + } + }, + ), + ) + ], + ); + } +} + +class CascadeController extends GetxController { + late List items = []; + @override + void onInit() { + items = []; + for (int i = 0; i < 5; i++) { + Item item0 = Item(); + item0.name = "name_$i"; + item0.code = "code_$i"; + List children1 = []; + for (int j = 0; j < 3; j++) { + Item item1 = Item(); + item1.name = "name_${i}_$j"; + item1.code = "code_${i}_$j"; + List children2 = []; + for (int k = 0; k < 7; k++) { + Item item2 = Item(); + item2.name = "name_${i}_${j}_$k"; + item2.code = "code_${i}_${j}_$k"; + // 第3页没有子数据列表 + item2.children = []; + children2.add(item2); + } + // 第2页的子数据列表 + item1.children = children2; + children1.add(item1); + } + // 第1页的子数据列表 + item0.children = children1; + items.add(item0); + super.onInit(); + } + } + + late final _CascadePickerState _state; + + _setState(_CascadePickerState state) { + _state = state; + } + + List get selectedTitles => _state._selectedTabs; + + List get selectedIndexes => _state._selectedIndexes; + + bool isCompleted() => + !_state._selectedTabs.contains(_CascadePickerState._newTabName); +} diff --git a/lib/widgets/core/sk_dialog_header.dart b/lib/widgets/core/sk_dialog_header.dart new file mode 100644 index 0000000..000786b --- /dev/null +++ b/lib/widgets/core/sk_dialog_header.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:sk_base_mobile/app_theme.dart'; +import 'package:sk_base_mobile/util/screen_adaper_util.dart'; + +class SkDialogHeader extends StatelessWidget { + final String title; + final Widget? leading; + final Widget? trailing; + const SkDialogHeader({ + super.key, + required this.title, + this.leading, + this.trailing, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(), + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: [AppTheme.primaryColorLight, AppTheme.primaryColor])), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + trailing ?? + SizedBox( + width: ScreenAdaper.width(100), + ), + Expanded( + child: Text( + title, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: ScreenAdaper.height(30), + color: AppTheme.nearlyWhite, + fontWeight: FontWeight.w600), + ), + ), + trailing ?? + Container( + width: ScreenAdaper.width(100), + child: IconButton( + onPressed: () { + Get.back(); + }, + icon: Icon( + Icons.close, + size: ScreenAdaper.sp(45), + color: AppTheme.nearlyWhite, + )), + ) + ], + ), + ); + } +} diff --git a/lib/widgets/core/sk_ink.dart b/lib/widgets/core/sk_ink.dart index b6e01a1..e9b5663 100644 --- a/lib/widgets/core/sk_ink.dart +++ b/lib/widgets/core/sk_ink.dart @@ -1,24 +1,48 @@ import 'package:flutter/material.dart'; import 'package:sk_base_mobile/app_theme.dart'; +import 'package:sk_base_mobile/constants/bg_color.dart'; +import 'package:sk_base_mobile/util/screen_adaper_util.dart'; class SkInk extends StatelessWidget { final Widget? child; final void Function()? onTap; final BorderRadius? borderRadius; final BoxBorder? border; + + final Color? color; + final EdgeInsets? padding; + final EdgeInsets? margin; + final Gradient? gradient; const SkInk( - {super.key, this.child, this.onTap, this.borderRadius, this.border}); + {super.key, + this.child, + this.onTap, + this.color, + this.borderRadius, + this.border, + this.gradient, + this.margin, + this.padding}); @override Widget build(BuildContext context) { - return Material( - child: Ink( - decoration: BoxDecoration( - border: border ?? Border.all(color: AppTheme.grey.withOpacity(0.8)), - borderRadius: borderRadius, - ), - child: - InkWell(borderRadius: borderRadius, onTap: onTap, child: child)), + return Container( + margin: margin, + child: ClipRRect( + borderRadius: borderRadius ?? BorderRadius.zero, + child: Material( + child: Ink( + padding: padding, + decoration: BoxDecoration( + border: border, + color: color, + gradient: gradient, + borderRadius: borderRadius, + ), + child: InkWell( + borderRadius: borderRadius, onTap: onTap, child: child)), + ), + ), ); } } diff --git a/lib/widgets/core/sk_tag.dart b/lib/widgets/core/sk_tag.dart index 61dc925..52359cc 100644 --- a/lib/widgets/core/sk_tag.dart +++ b/lib/widgets/core/sk_tag.dart @@ -1,4 +1,3 @@ -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:sk_base_mobile/app_theme.dart'; import 'package:sk_base_mobile/util/screen_adaper_util.dart'; diff --git a/lib/widgets/core/sk_text_input.dart b/lib/widgets/core/sk_text_input.dart index adaada7..6756354 100644 --- a/lib/widgets/core/sk_text_input.dart +++ b/lib/widgets/core/sk_text_input.dart @@ -20,6 +20,7 @@ class SkTextInput extends StatefulWidget { final Widget? suffixIcon; final InputBorder? border; final FloatingLabelBehavior? floatingLabelBehavior; + final TextInputType? keyboardType; const SkTextInput( {super.key, required this.textController, @@ -28,6 +29,7 @@ class SkTextInput extends StatefulWidget { this.onFieldSubmitted, this.isRequired = false, this.onTapOutside, + this.keyboardType, this.labelText, this.prefix, this.suffixIcon, @@ -79,6 +81,7 @@ class _SkTextInputState extends State { widget.onTap!(focusNode); } }, + keyboardType: widget.keyboardType, onFieldSubmitted: widget.onFieldSubmitted, autovalidateMode: AutovalidateMode.onUserInteraction, validator: widget.validator, diff --git a/lib/widgets/fade_in_cache_image.dart b/lib/widgets/fade_in_cache_image.dart index 52be026..0326cf1 100644 --- a/lib/widgets/fade_in_cache_image.dart +++ b/lib/widgets/fade_in_cache_image.dart @@ -54,11 +54,17 @@ class _FadeInCacheImageState extends State { Widget buildImg(String? url) { return CachedNetworkImage( - alignment: Alignment.center, imageUrl: url ?? '', width: widget.width, height: widget.height, - fit: widget.fit, + imageBuilder: (context, imageProvider) => Container( + decoration: BoxDecoration( + image: DecorationImage( + image: imageProvider, + fit: widget.fit ?? BoxFit.cover, + ), + ), + ), placeholder: (context, url) => Container( decoration: const BoxDecoration(color: AppTheme.grey), child: const CupertinoActivityIndicator(), @@ -70,12 +76,10 @@ class _FadeInCacheImageState extends State { Widget defaultImg() { return Container( alignment: Alignment.center, - decoration: BoxDecoration( - border: Border.all(), borderRadius: BorderRadius.circular(15)), width: widget.width, height: widget.height, child: Icon(Icons.image_not_supported, - size: ScreenAdaper.height((widget.width ?? 200) * 3 / 4), + size: ScreenAdaper.height((widget.width ?? 200)), color: AppTheme.grey), ); } diff --git a/lib/widgets/gradient_button.dart b/lib/widgets/gradient_button.dart index d90220c..363791a 100644 --- a/lib/widgets/gradient_button.dart +++ b/lib/widgets/gradient_button.dart @@ -3,63 +3,100 @@ import 'package:loading_animation_widget/loading_animation_widget.dart'; import 'package:sk_base_mobile/app_theme.dart'; import 'package:sk_base_mobile/constants/constants.dart'; import 'package:sk_base_mobile/util/screen_adaper_util.dart'; +import 'package:sk_base_mobile/widgets/core/sk_ink.dart'; class GradientButton extends StatelessWidget { final VoidCallback? onPressed; final bool isLoading; final String buttonText; final Icon? icon; + final BorderRadiusGeometry? borderRadius; const GradientButton( {super.key, this.buttonText = TextEnum.createInventoryInOutBtnText, this.onPressed, this.icon, + this.borderRadius, this.isLoading = false}); @override Widget build(BuildContext context) { - return SizedBox( - width: double.infinity, - child: ElevatedButton( - style: ButtonStyle( - backgroundColor: - MaterialStateProperty.all(AppTheme.primaryColor), - padding: MaterialStateProperty.all( - EdgeInsets.symmetric(vertical: ScreenAdaper.height(10)), - ), - minimumSize: MaterialStateProperty.all( - Size(double.infinity, ScreenAdaper.height(80))), - shape: MaterialStateProperty.all( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - ), - ), - onPressed: onPressed ?? () => {}, - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (icon != null) ...[ - icon!, - SizedBox( - width: ScreenAdaper.width(10), - ) - ], - isLoading - ? LoadingAnimationWidget.fourRotatingDots( - color: AppTheme.nearlyWhite, - size: ScreenAdaper.height(40), - ) - : Text( - buttonText, - style: TextStyle( + return SkInk( + onTap: onPressed ?? () {}, + gradient: const LinearGradient( + colors: [AppTheme.primaryColorLight, AppTheme.primaryColor]), + child: SizedBox( + height: ScreenAdaper.height(80), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (icon != null) ...[ + icon!, + SizedBox( + width: ScreenAdaper.width(10), + ) + ], + isLoading + ? LoadingAnimationWidget.fourRotatingDots( color: AppTheme.nearlyWhite, - fontWeight: FontWeight.bold, - fontSize: ScreenAdaper.height(25), - ), - ) - ]), - ), + size: ScreenAdaper.height(40), + ) + : Text( + buttonText, + style: TextStyle( + color: AppTheme.nearlyWhite, + fontWeight: FontWeight.bold, + fontSize: ScreenAdaper.height(25), + ), + ) + ])), ); + + // ElevatedButton( + // style: ButtonStyle( + // backgroundColor: + // MaterialStateProperty.all(AppTheme.primaryColor), + // minimumSize: MaterialStateProperty.all( + // Size(double.infinity, ScreenAdaper.height(80))), + // shape: MaterialStateProperty.all( + // RoundedRectangleBorder( + // borderRadius: borderRadius ?? BorderRadius.circular(10), + // ), + // ), + // ), + // onPressed: onPressed ?? () => {}, + // child: Text( + // buttonText, + // style: TextStyle( + // color: AppTheme.nearlyWhite, + // fontWeight: FontWeight.bold, + // fontSize: ScreenAdaper.height(25), + // ), + // ), + // child: Row( + // crossAxisAlignment: CrossAxisAlignment.center, + // mainAxisAlignment: MainAxisAlignment.center, + // children: [ + // if (icon != null) ...[ + // icon!, + // SizedBox( + // width: ScreenAdaper.width(10), + // ) + // ], + // isLoading + // ? LoadingAnimationWidget.fourRotatingDots( + // color: AppTheme.nearlyWhite, + // size: ScreenAdaper.height(40), + // ) + // : Text( + // buttonText, + // style: TextStyle( + // color: AppTheme.nearlyWhite, + // fontWeight: FontWeight.bold, + // fontSize: ScreenAdaper.height(25), + // ), + // ) + // ]), + // ); } } diff --git a/lib/widgets/loading_indicator.dart b/lib/widgets/loading_indicator.dart index c854792..e741043 100644 --- a/lib/widgets/loading_indicator.dart +++ b/lib/widgets/loading_indicator.dart @@ -14,10 +14,13 @@ class LoadingIndicator extends StatelessWidget { return common ? LoadingAnimationWidget.fourRotatingDots( color: AppTheme.primaryColorLight, size: ScreenAdaper.height(45)) - : CupertinoActivityIndicator( - animating: animating, - color: AppTheme.primaryColor, - radius: ScreenAdaper.sp(25), + : Container( + padding: EdgeInsets.all(ScreenAdaper.height(10)), + child: CupertinoActivityIndicator( + animating: animating, + color: AppTheme.primaryColor, + radius: ScreenAdaper.sp(25), + ), ); } } diff --git a/lib/widgets/my_avatar.dart b/lib/widgets/my_avatar.dart index 97a45b1..5cb18ed 100644 --- a/lib/widgets/my_avatar.dart +++ b/lib/widgets/my_avatar.dart @@ -1,9 +1,11 @@ import 'dart:io'; +import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:image_picker/image_picker.dart'; import 'package:sk_base_mobile/store/auth.store.dart'; import 'package:sk_base_mobile/app_theme.dart'; +import 'package:sk_base_mobile/widgets/fade_in_cache_image.dart'; import '../util/util.dart'; @@ -15,17 +17,7 @@ class MyAvatarWidget extends StatelessWidget { Widget build(BuildContext context) { return Stack( children: [ - Obx(() => Container( - decoration: BoxDecoration( - image: - DecorationImage(fit: BoxFit.cover, image: _buildImage()), - border: Border.all( - color: AppTheme.white, width: ScreenAdaper.sp(2)), - borderRadius: BorderRadius.circular(50)), - height: ScreenAdaper.width(150), - width: ScreenAdaper.width(150), - child: const SizedBox(), - )), + Obx(() => _buildAvatar()), Positioned( bottom: 0, right: 0, @@ -41,11 +33,16 @@ class MyAvatarWidget extends StatelessWidget { ); } - dynamic _buildImage() { + dynamic _buildAvatar() { return _controller.uploadImgFilePath.value.isNotEmpty ? FileImage(File(_controller.uploadImgFilePath.value)) - : NetworkImage( - AuthStore.to.userInfo.value.avatar ?? '', + : ClipRRect( + borderRadius: BorderRadius.circular(ScreenAdaper.sp(120)), + child: FadeInCacheImage( + height: ScreenAdaper.height(120), + width: ScreenAdaper.height(120), + url: MediaUtil.getMediaUrl(AuthStore.to.userInfo.value.avatar), + ), ); } // Widget getShowImg() { diff --git a/lib/widgets/upgrade_confirm.dart b/lib/widgets/upgrade_confirm.dart index e8597e1..95d453b 100644 --- a/lib/widgets/upgrade_confirm.dart +++ b/lib/widgets/upgrade_confirm.dart @@ -67,7 +67,7 @@ class UpgradeConfirm extends StatelessWidget { SizedBox( height: ScreenAdaper.height(30), ), - Container( + SizedBox( width: ScreenAdaper.height(400), child: GradientButton( buttonText: '去更新', diff --git a/pubspec.lock b/pubspec.lock index b098a7f..a180c01 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1005,6 +1005,70 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.2" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: "0ecc004c62fd3ed36a2ffcbe0dd9700aee63bd7532d0b642a488b1ec310f492e" + url: "https://pub.dev" + source: hosted + version: "6.2.5" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: d4ed0711849dd8e33eb2dd69c25db0d0d3fdc37e0a62e629fe32f57a22db2745 + url: "https://pub.dev" + source: hosted + version: "6.3.0" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "9149d493b075ed740901f3ee844a38a00b33116c7c5c10d7fb27df8987fb51d5" + url: "https://pub.dev" + source: hosted + version: "6.2.5" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811 + url: "https://pub.dev" + source: hosted + version: "3.1.1" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234 + url: "https://pub.dev" + source: hosted + version: "3.1.0" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: fff0932192afeedf63cdd50ecbb1bc825d31aed259f02bb8dba0f3b729a5e88b + url: "https://pub.dev" + source: hosted + version: "2.2.3" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7 + url: "https://pub.dev" + source: hosted + version: "3.1.1" uuid: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 84493cd..e6be3f6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -67,6 +67,7 @@ dependencies: pinyin: ^3.2.0 math_expressions: ^2.4.0 install_plugin: ^2.1.0 + url_launcher: ^6.2.5 dev_dependencies: flutter_test: sdk: flutter