diff --git a/assets/icons/contract.svg b/assets/icons/contract.svg new file mode 100644 index 0000000..acf3785 --- /dev/null +++ b/assets/icons/contract.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/hr.svg b/assets/icons/hr.svg new file mode 100644 index 0000000..c4a3fb4 --- /dev/null +++ b/assets/icons/hr.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/inventory.svg b/assets/icons/inventory.svg new file mode 100644 index 0000000..f2f9b89 --- /dev/null +++ b/assets/icons/inventory.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/product.svg b/assets/icons/product.svg new file mode 100644 index 0000000..33595d8 --- /dev/null +++ b/assets/icons/product.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/report.svg b/assets/icons/report.svg new file mode 100644 index 0000000..6c88a12 --- /dev/null +++ b/assets/icons/report.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/sale_quotation.svg b/assets/icons/sale_quotation.svg new file mode 100644 index 0000000..4d6ab77 --- /dev/null +++ b/assets/icons/sale_quotation.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/task_manage.svg b/assets/icons/task_manage.svg new file mode 100644 index 0000000..8ca2cb2 --- /dev/null +++ b/assets/icons/task_manage.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/vehicle.svg b/assets/icons/vehicle.svg new file mode 100644 index 0000000..326d7f0 --- /dev/null +++ b/assets/icons/vehicle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/lib/app_theme.dart b/lib/app_theme.dart index 9ea8308..859d638 100644 --- a/lib/app_theme.dart +++ b/lib/app_theme.dart @@ -93,7 +93,7 @@ final theme = ThemeData( )), appBarTheme: AppBarTheme( centerTitle: true, - iconTheme: const IconThemeData(color: Colors.black), + iconTheme: const IconThemeData(color: Colors.white), backgroundColor: AppTheme.primaryColor, titleTextStyle: TextStyle( color: Colors.white, diff --git a/lib/config.dart b/lib/config.dart index 02b0257..64a31e5 100644 --- a/lib/config.dart +++ b/lib/config.dart @@ -5,10 +5,10 @@ class GloablConfig { // static const BASE_URL = "http://10.0.2.2:8001/api/"; // static const OSS_URL = "http://10.0.2.2:8001"; - static const BASE_URL = "http://144.123.43.138:3001/api/"; - static const OSS_URL = "http://144.123.43.138:3001"; - // static const BASE_URL = "http://192.168.60.220:8001/api/"; - // static const OSS_URL = "http://192.168.60.220:8001"; + // static const BASE_URL = "http://144.123.43.138:3001/api/"; + // static const OSS_URL = "http://144.123.43.138:3001"; + static const BASE_URL = "http://192.168.60.220:8001/api/"; + static const OSS_URL = "http://192.168.60.220:8001"; static const DOMAIN_NAME = "山矿通"; static const DEBUG = true; static const PRIVACY_POLICY = 'http://h5.heeru.xyz/privacyPolicy.html'; diff --git a/lib/constants/router.dart b/lib/constants/router.dart index 94fc7fb..cba8d47 100644 --- a/lib/constants/router.dart +++ b/lib/constants/router.dart @@ -1,6 +1,7 @@ import 'package:get/get.dart'; import 'package:sk_base_mobile/screens/inventory/inventory.dart'; import 'package:sk_base_mobile/screens/login/login.dart'; +import 'package:sk_base_mobile/screens/sale_quotation/sale_quotation.dart'; import '../screens/landing/landing.dart'; import '../screens/mine/useinfo/userinfo.dart'; @@ -10,10 +11,13 @@ class RouteConfig { static const String login = '/login'; static const String userinfo = '/userinfo'; static const String inventory = '/inventory'; + static const String saleQuotation = '/sale_quotation'; + static final List getPages = [ GetPage(name: login, page: () => LoginScreen()), GetPage(name: home, page: () => LandingPage()), GetPage(name: userinfo, page: () => UserInfoPage()), GetPage(name: inventory, page: () => const InventoryPage()), + GetPage(name: saleQuotation, page: () => const SaleQuotationPage()) ]; } diff --git a/lib/models/workbench.model.dart b/lib/models/workbench.model.dart new file mode 100644 index 0000000..4af6efa --- /dev/null +++ b/lib/models/workbench.model.dart @@ -0,0 +1,7 @@ +class WorkBenchModel { + final String title; + final String icon; + final String route; + WorkBenchModel( + {required this.title, required this.route, required this.icon}); +} diff --git a/lib/screens/inventory_inout/components/custom_app_bar.dart b/lib/screens/inventory_inout/components/custom_app_bar.dart index 68cb19b..e7bce3b 100644 --- a/lib/screens/inventory_inout/components/custom_app_bar.dart +++ b/lib/screens/inventory_inout/components/custom_app_bar.dart @@ -6,7 +6,7 @@ import 'package:sk_base_mobile/constants/constants.dart'; import 'package:sk_base_mobile/screens/inventory_inout/inventory_inout_controller.dart'; import 'package:sk_base_mobile/store/auth.store.dart'; import 'package:sk_base_mobile/util/screen_adaper_util.dart'; -import 'package:sk_base_mobile/widgets/core/zt_base_date_picker.dart'; +import 'package:sk_base_mobile/widgets/core/sk_base_date_picker.dart'; class CustomAppBar extends StatelessWidget { CustomAppBar({super.key}); @@ -59,7 +59,7 @@ class CustomAppBar extends StatelessWidget { showCupertinoModalPopup( context: Get.overlayContext!, builder: (BuildContext context) { - return ZtBaseDatePicker( + return SkBaseDatePicker( initialDate: controller.endTime.value, endDate: controller.endTime.value, onDateTimeChanged: (date) { diff --git a/lib/screens/inventory_inout/inventory_inout.dart b/lib/screens/inventory_inout/inventory_inout.dart index 1178b9f..0e937d0 100644 --- a/lib/screens/inventory_inout/inventory_inout.dart +++ b/lib/screens/inventory_inout/inventory_inout.dart @@ -14,6 +14,10 @@ class InvenotryInoutPage extends StatelessWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + // 工具栏的高度 + SizedBox( + height: MediaQuery.of(context).padding.top, + ), UperBody(), const Expanded( child: TaskPageBody(), diff --git a/lib/screens/landing/landing.dart b/lib/screens/landing/landing.dart index 1fee2f9..3c34c65 100644 --- a/lib/screens/landing/landing.dart +++ b/lib/screens/landing/landing.dart @@ -16,21 +16,19 @@ class LandingPage extends StatelessWidget { return Material( child: Stack(children: [ const BackColors(), - SafeArea( - bottom: false, - child: Scaffold( - floatingActionButtonLocation: - FloatingActionButtonLocation.centerDocked, - // floatingActionButton: [0].indexWhere( - // (item) => item == controller.currentIndex.value) > - // -1 - // ? FloatingCreateButton() - // : null, - floatingActionButton: FloatingCreateButton(), - bottomNavigationBar: BottomNavBar(), - backgroundColor: Colors.transparent, - body: Obx(() => controller.pages[controller.currentIndex.value]), - )) + Scaffold( + floatingActionButtonLocation: + FloatingActionButtonLocation.centerDocked, + // floatingActionButton: [0].indexWhere( + // (item) => item == controller.currentIndex.value) > + // -1 + // ? FloatingCreateButton() + // : null, + floatingActionButton: FloatingCreateButton(), + bottomNavigationBar: BottomNavBar(), + backgroundColor: Colors.transparent, + body: Obx(() => controller.pages[controller.currentIndex.value]), + ) ]), ); } diff --git a/lib/screens/new_inventory_inout/components/inventory_search.dart b/lib/screens/new_inventory_inout/components/inventory_search.dart index 462f5b7..fe4662c 100644 --- a/lib/screens/new_inventory_inout/components/inventory_search.dart +++ b/lib/screens/new_inventory_inout/components/inventory_search.dart @@ -124,7 +124,7 @@ class InventorySearch extends StatelessWidget { // return Container( // width: ScreenAdaper.width(200), // constraints: BoxConstraints(minWidth: ScreenAdaper.width(200)), - // child: ZtSearchSelect( + // child: SkSearchSelect( // isRequired: true, // contentPadding: // EdgeInsets.symmetric(horizontal: ScreenAdaper.width(15)), diff --git a/lib/screens/new_inventory_inout/new_inventory_inout.dart b/lib/screens/new_inventory_inout/new_inventory_inout.dart index b7404bb..2bc3a80 100644 --- a/lib/screens/new_inventory_inout/new_inventory_inout.dart +++ b/lib/screens/new_inventory_inout/new_inventory_inout.dart @@ -13,10 +13,10 @@ import 'package:sk_base_mobile/screens/new_inventory_inout/components/inventory_ import 'package:sk_base_mobile/screens/new_inventory_inout/components/product_search.dart'; import 'package:sk_base_mobile/store/dict.store.dart'; import 'package:sk_base_mobile/util/util.dart'; -import 'package:sk_base_mobile/widgets/core/zt_number_input.dart'; -import 'package:sk_base_mobile/widgets/core/zt_search_select.dart'; -import 'package:sk_base_mobile/widgets/core/zk_date_picker.dart'; -import 'package:sk_base_mobile/widgets/core/zt_text_input.dart'; +import 'package:sk_base_mobile/widgets/core/sk_number_input.dart'; +import 'package:sk_base_mobile/widgets/core/sk_search_select.dart'; +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'; @@ -180,7 +180,7 @@ class NewInventoryInout extends StatelessWidget { /// 项目 Widget buildProjectPicker() { - return ZtSearchSelect( + return SkSearchSelect( isRequired: true, textController: controller.projectTextController, labelText: '项目', @@ -278,7 +278,7 @@ class NewInventoryInout extends StatelessWidget { /// 时间 Widget buildDatePicker() { - return ZtDatePicker( + return SkDatePicker( textController: controller.dateTextController, onClear: () { controller.payload.remove('time'); @@ -294,7 +294,7 @@ class NewInventoryInout extends StatelessWidget { /// 数量 Widget buildQuantity() { - return ZtNumberInput( + return SkNumberInput( textController: controller.quantityTextController, labelText: '数量', onChanged: (value) { @@ -306,7 +306,7 @@ class NewInventoryInout extends StatelessWidget { /// 单价 Widget buildUnitPrice() { - return ZtNumberInput( + return SkNumberInput( textController: controller.unitPriceTextController, labelText: '单价', onChanged: (value) { @@ -317,7 +317,7 @@ class NewInventoryInout extends StatelessWidget { /// 金额 Widget buildAmount() { - return ZtNumberInput( + return SkNumberInput( textController: controller.amountTextController, labelText: '金额', onChanged: (value) { @@ -375,7 +375,7 @@ class NewInventoryInout extends StatelessWidget { /// 备注 Widget buildRemark() { - return ZtTextInput( + return SkTextInput( textController: controller.remarkTextController, labelText: '备注', isTextArea: true, diff --git a/lib/screens/sale_quotation/components/data_sources.dart b/lib/screens/sale_quotation/components/data_sources.dart new file mode 100644 index 0000000..ab529c2 --- /dev/null +++ b/lib/screens/sale_quotation/components/data_sources.dart @@ -0,0 +1,692 @@ +// ignore_for_file: avoid_print + +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:data_table_2/data_table_2.dart'; + +// Copyright 2019 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// The file was extracted from GitHub: https://github.com/flutter/gallery +// Changes and modifications by Maxim Saplin, 2021 + +/// Keeps track of selected rows, feed the data into DesertsDataSource +class RestorableDessertSelections extends RestorableProperty> { + Set _dessertSelections = {}; + + /// Returns whether or not a dessert row is selected by index. + bool isSelected(int index) => _dessertSelections.contains(index); + + /// Takes a list of [Dessert]s and saves the row indices of selected rows + /// into a [Set]. + void setDessertSelections(List desserts) { + final updatedSet = {}; + for (var i = 0; i < desserts.length; i += 1) { + var dessert = desserts[i]; + if (dessert.selected) { + updatedSet.add(i); + } + } + _dessertSelections = updatedSet; + notifyListeners(); + } + + @override + Set createDefaultValue() => _dessertSelections; + + @override + Set fromPrimitives(Object? data) { + final selectedItemIndices = data as List; + _dessertSelections = { + ...selectedItemIndices.map((dynamic id) => id as int), + }; + return _dessertSelections; + } + + @override + void initWithValue(Set value) { + _dessertSelections = value; + } + + @override + Object toPrimitives() => _dessertSelections.toList(); +} + +int _idCounter = 0; + +/// Domain model entity +class Dessert { + Dessert( + this.name, + this.calories, + this.fat, + this.carbs, + this.protein, + this.sodium, + this.calcium, + this.iron, + ); + + final int id = _idCounter++; + + final String name; + final int calories; + final double fat; + final int carbs; + final double protein; + final int sodium; + final int calcium; + final int iron; + bool selected = false; +} + +/// Data source implementing standard Flutter's DataTableSource abstract class +/// which is part of DataTable and PaginatedDataTable synchronous data fecthin API. +/// This class uses static collection of deserts as a data store, projects it into +/// DataRows, keeps track of selected items, provides sprting capability +class DessertDataSource extends DataTableSource { + DessertDataSource.empty(this.context) { + desserts = []; + } + + DessertDataSource(this.context, + [sortedByCalories = false, + this.hasRowTaps = false, + this.hasRowHeightOverrides = false, + this.hasZebraStripes = false]) { + desserts = _desserts; + if (sortedByCalories) { + sort((d) => d.calories, true); + } + } + + final BuildContext context; + late List desserts; + // Add row tap handlers and show snackbar + bool hasRowTaps = false; + // Override height values for certain rows + bool hasRowHeightOverrides = false; + // Color each Row by index's parity + bool hasZebraStripes = false; + + void sort(Comparable Function(Dessert d) getField, bool ascending) { + desserts.sort((a, b) { + final aValue = getField(a); + final bValue = getField(b); + return ascending + ? Comparable.compare(aValue, bValue) + : Comparable.compare(bValue, aValue); + }); + notifyListeners(); + } + + void updateSelectedDesserts(RestorableDessertSelections selectedRows) { + _selectedCount = 0; + for (var i = 0; i < desserts.length; i += 1) { + var dessert = desserts[i]; + if (selectedRows.isSelected(i)) { + dessert.selected = true; + _selectedCount += 1; + } else { + dessert.selected = false; + } + } + notifyListeners(); + } + + @override + DataRow2 getRow(int index, [Color? color]) { + final format = NumberFormat.decimalPercentPattern( + locale: 'en', + decimalDigits: 0, + ); + assert(index >= 0); + if (index >= desserts.length) throw 'index > _desserts.length'; + final dessert = desserts[index]; + return DataRow2.byIndex( + index: index, + selected: dessert.selected, + color: color != null + ? MaterialStateProperty.all(color) + : (hasZebraStripes && index.isEven + ? MaterialStateProperty.all(Theme.of(context).highlightColor) + : null), + onSelectChanged: (value) { + if (dessert.selected != value) { + _selectedCount += value! ? 1 : -1; + assert(_selectedCount >= 0); + dessert.selected = value; + notifyListeners(); + } + }, + onTap: hasRowTaps + ? () => _showSnackbar(context, 'Tapped on row ${dessert.name}') + : null, + onDoubleTap: hasRowTaps + ? () => _showSnackbar(context, 'Double Tapped on row ${dessert.name}') + : null, + onLongPress: hasRowTaps + ? () => _showSnackbar(context, 'Long pressed on row ${dessert.name}') + : null, + onSecondaryTap: hasRowTaps + ? () => _showSnackbar(context, 'Right clicked on row ${dessert.name}') + : null, + onSecondaryTapDown: hasRowTaps + ? (d) => + _showSnackbar(context, 'Right button down on row ${dessert.name}') + : null, + specificRowHeight: + hasRowHeightOverrides && dessert.fat >= 25 ? 100 : null, + cells: [ + DataCell(Text(dessert.name)), + DataCell(Text('${dessert.calories}'), + onTap: () => _showSnackbar(context, + 'Tapped on a cell with "${dessert.calories}"', Colors.red)), + DataCell(Text(dessert.fat.toStringAsFixed(1))), + DataCell(Text('${dessert.carbs}')), + DataCell(Text(dessert.protein.toStringAsFixed(1))), + DataCell(Text('${dessert.sodium}')), + DataCell(Text(format.format(dessert.calcium / 100))), + DataCell(Text(format.format(dessert.iron / 100))), + ], + ); + } + + @override + int get rowCount => desserts.length; + + @override + bool get isRowCountApproximate => false; + + @override + int get selectedRowCount => _selectedCount; + + void selectAll(bool? checked) { + for (final dessert in desserts) { + dessert.selected = checked ?? false; + } + _selectedCount = (checked ?? false) ? desserts.length : 0; + notifyListeners(); + } +} + +/// Async datasource for AsynPaginatedDataTabke2 example. Based on AsyncDataTableSource which +/// is an extension to Flutter's DataTableSource and aimed at solving +/// saync data fetching scenarious by paginated table (such as using Web API) +class DessertDataSourceAsync extends AsyncDataTableSource { + DessertDataSourceAsync() { + print('DessertDataSourceAsync created'); + } + + DessertDataSourceAsync.empty() { + _empty = true; + print('DessertDataSourceAsync.empty created'); + } + + DessertDataSourceAsync.error() { + _errorCounter = 0; + print('DessertDataSourceAsync.error created'); + } + + bool _empty = false; + int? _errorCounter; + + RangeValues? _caloriesFilter; + + RangeValues? get caloriesFilter => _caloriesFilter; + set caloriesFilter(RangeValues? calories) { + _caloriesFilter = calories; + refreshDatasource(); + } + + final DesertsFakeWebService _repo = DesertsFakeWebService(); + + String _sortColumn = "name"; + bool _sortAscending = true; + + void sort(String columnName, bool ascending) { + _sortColumn = columnName; + _sortAscending = ascending; + refreshDatasource(); + } + + Future getTotalRecords() { + return Future.delayed( + const Duration(milliseconds: 0), () => _empty ? 0 : _dessertsX3.length); + } + + @override + Future getRows(int startIndex, int count) async { + print('getRows($startIndex, $count)'); + if (_errorCounter != null) { + _errorCounter = _errorCounter! + 1; + + if (_errorCounter! % 2 == 1) { + await Future.delayed(const Duration(milliseconds: 1000)); + throw 'Error #${((_errorCounter! - 1) / 2).round() + 1} has occured'; + } + } + + final format = NumberFormat.decimalPercentPattern( + locale: 'en', + decimalDigits: 0, + ); + assert(startIndex >= 0); + + // List returned will be empty is there're fewer items than startingAt + var x = _empty + ? await Future.delayed(const Duration(milliseconds: 2000), + () => DesertsFakeWebServiceResponse(0, [])) + : await _repo.getData( + startIndex, count, _caloriesFilter, _sortColumn, _sortAscending); + + var r = AsyncRowsResponse( + x.totalRecords, + x.data.map((dessert) { + return DataRow( + key: ValueKey(dessert.id), + //selected: dessert.selected, + onSelectChanged: (value) { + if (value != null) { + setRowSelection(ValueKey(dessert.id), value); + } + }, + cells: [ + DataCell(Text(dessert.name)), + DataCell(Text('${dessert.calories}')), + DataCell(Text(dessert.fat.toStringAsFixed(1))), + DataCell(Text('${dessert.carbs}')), + DataCell(Text(dessert.protein.toStringAsFixed(1))), + DataCell(Text('${dessert.sodium}')), + DataCell(Text(format.format(dessert.calcium / 100))), + DataCell(Text(format.format(dessert.iron / 100))), + ], + ); + }).toList()); + + return r; + } +} + +class DesertsFakeWebServiceResponse { + DesertsFakeWebServiceResponse(this.totalRecords, this.data); + + /// THe total ammount of records on the server, e.g. 100 + final int totalRecords; + + /// One page, e.g. 10 reocrds + final List data; +} + +class DesertsFakeWebService { + int Function(Dessert, Dessert)? _getComparisonFunction( + String column, bool ascending) { + var coef = ascending ? 1 : -1; + switch (column) { + case 'name': + return (Dessert d1, Dessert d2) => coef * d1.name.compareTo(d2.name); + case 'calories': + return (Dessert d1, Dessert d2) => coef * (d1.calories - d2.calories); + case 'fat': + return (Dessert d1, Dessert d2) => coef * (d1.fat - d2.fat).round(); + case 'carbs': + return (Dessert d1, Dessert d2) => coef * (d1.carbs - d2.carbs); + case 'protein': + return (Dessert d1, Dessert d2) => + coef * (d1.protein - d2.protein).round(); + case 'sodium': + return (Dessert d1, Dessert d2) => coef * (d1.sodium - d2.sodium); + case 'calcium': + return (Dessert d1, Dessert d2) => coef * (d1.calcium - d2.calcium); + case 'iron': + return (Dessert d1, Dessert d2) => coef * (d1.iron - d2.iron); + } + + return null; + } + + Future getData(int startingAt, int count, + RangeValues? caloriesFilter, String sortedBy, bool sortedAsc) async { + return Future.delayed( + Duration( + milliseconds: startingAt == 0 + ? 2650 + : startingAt < 20 + ? 2000 + : 400), () { + var result = _dessertsX3; + + if (caloriesFilter != null) { + result = result + .where((e) => + e.calories >= caloriesFilter.start && + e.calories <= caloriesFilter.end) + .toList(); + } + + result.sort(_getComparisonFunction(sortedBy, sortedAsc)); + return DesertsFakeWebServiceResponse( + result.length, result.skip(startingAt).take(count).toList()); + }); + } +} + +int _selectedCount = 0; + +List _desserts = [ + Dessert( + 'Frozen Yogurt', + 159, + 6.0, + 24, + 4.0, + 87, + 14, + 1, + ), + Dessert( + 'Ice Cream Sandwich', + 237, + 9.0, + 37, + 4.3, + 129, + 8, + 1, + ), + Dessert( + 'Eclair', + 262, + 16.0, + 24, + 6.0, + 337, + 6, + 7, + ), + Dessert( + 'Cupcake', + 305, + 3.7, + 67, + 4.3, + 413, + 3, + 8, + ), + Dessert( + 'Gingerbread', + 356, + 16.0, + 49, + 3.9, + 327, + 7, + 16, + ), + Dessert( + 'Jelly Bean', + 375, + 0.0, + 94, + 0.0, + 50, + 0, + 0, + ), + Dessert( + 'Lollipop', + 392, + 0.2, + 98, + 0.0, + 38, + 0, + 2, + ), + Dessert( + 'Honeycomb', + 408, + 3.2, + 87, + 6.5, + 562, + 0, + 45, + ), + Dessert( + 'Donut', + 452, + 25.0, + 51, + 4.9, + 326, + 2, + 22, + ), + Dessert( + 'Apple Pie', + 518, + 26.0, + 65, + 7.0, + 54, + 12, + 6, + ), + Dessert( + 'Frozen Yougurt with sugar', + 168, + 6.0, + 26, + 4.0, + 87, + 14, + 1, + ), + Dessert( + 'Ice Cream Sandwich with sugar', + 246, + 9.0, + 39, + 4.3, + 129, + 8, + 1, + ), + Dessert( + 'Eclair with sugar', + 271, + 16.0, + 26, + 6.0, + 337, + 6, + 7, + ), + Dessert( + 'Cupcake with sugar', + 314, + 3.7, + 69, + 4.3, + 413, + 3, + 8, + ), + Dessert( + 'Gingerbread with sugar', + 345, + 16.0, + 51, + 3.9, + 327, + 7, + 16, + ), + Dessert( + 'Jelly Bean with sugar', + 364, + 0.0, + 96, + 0.0, + 50, + 0, + 0, + ), + Dessert( + 'Lollipop with sugar', + 401, + 0.2, + 100, + 0.0, + 38, + 0, + 2, + ), + Dessert( + 'Honeycomd with sugar', + 417, + 3.2, + 89, + 6.5, + 562, + 0, + 45, + ), + Dessert( + 'Donut with sugar', + 461, + 25.0, + 53, + 4.9, + 326, + 2, + 22, + ), + Dessert( + 'Apple pie with sugar', + 527, + 26.0, + 67, + 7.0, + 54, + 12, + 6, + ), + Dessert( + 'Forzen yougurt with honey', + 223, + 6.0, + 36, + 4.0, + 87, + 14, + 1, + ), + Dessert( + 'Ice Cream Sandwich with honey', + 301, + 9.0, + 49, + 4.3, + 129, + 8, + 1, + ), + Dessert( + 'Eclair with honey', + 326, + 16.0, + 36, + 6.0, + 337, + 6, + 7, + ), + Dessert( + 'Cupcake with honey', + 369, + 3.7, + 79, + 4.3, + 413, + 3, + 8, + ), + Dessert( + 'Gignerbread with hone', + 420, + 16.0, + 61, + 3.9, + 327, + 7, + 16, + ), + Dessert( + 'Jelly Bean with honey', + 439, + 0.0, + 106, + 0.0, + 50, + 0, + 0, + ), + Dessert( + 'Lollipop with honey', + 456, + 0.2, + 110, + 0.0, + 38, + 0, + 2, + ), + Dessert( + 'Honeycomd with honey', + 472, + 3.2, + 99, + 6.5, + 562, + 0, + 45, + ), + Dessert( + 'Donut with honey', + 516, + 25.0, + 63, + 4.9, + 326, + 2, + 22, + ), + Dessert( + 'Apple pie with honey', + 582, + 26.0, + 77, + 7.0, + 54, + 12, + 6, + ), +]; + +List _dessertsX3 = _desserts.toList() + ..addAll(_desserts.map((i) => Dessert('${i.name} x2', i.calories, i.fat, + i.carbs, i.protein, i.sodium, i.calcium, i.iron))) + ..addAll(_desserts.map((i) => Dessert('${i.name} x3', i.calories, i.fat, + i.carbs, i.protein, i.sodium, i.calcium, i.iron))); + +_showSnackbar(BuildContext context, String text, [Color? color]) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + backgroundColor: color, + duration: const Duration(seconds: 1), + content: Text(text), + )); +} diff --git a/lib/screens/sale_quotation/components/data_table.dart b/lib/screens/sale_quotation/components/data_table.dart new file mode 100644 index 0000000..288367e --- /dev/null +++ b/lib/screens/sale_quotation/components/data_table.dart @@ -0,0 +1,755 @@ +import 'package:data_table_2/data_table_2.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_text_input.dart'; +import './helper.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; + +import './data_sources.dart'; +import './nav_helper.dart'; + +// Copyright 2019 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// The file was extracted from GitHub: https://github.com/flutter/gallery +// Changes and modifications by Maxim Saplin, 2021 + +class DataTable2FixedNMDemo extends StatefulWidget { + const DataTable2FixedNMDemo({super.key}); + + @override + DataTable2FixedNMDemoState createState() => DataTable2FixedNMDemoState(); +} + +class DataTable2FixedNMDemoState extends State { + bool _sortAscending = true; + int? _sortColumnIndex; + late DessertDataSource _dessertsDataSource; + late DessertDataSourceAsync _asyncDessertsDataSource; + bool _initialized = false; + final ScrollController _controller = ScrollController(); + String selectedTableType = dflt; + + int _fixedRows = 1; + int _fixedCols = 1; + int _dataItems = 30; + + DataRow _getRow(int index, [Color? color]) { + final format = NumberFormat.decimalPercentPattern( + locale: 'zh', + decimalDigits: 0, + ); + assert(index >= 0); + if (index >= _desserts.length) throw 'index > _desserts.length'; + final dessert = _desserts[index]; + return DataRow2.byIndex( + index: index, + // selected: dessert.selected, + color: color != null ? MaterialStateProperty.all(color) : null, + // onSelectChanged: (value) { + // if (dessert.selected != value) { + // dessert.selected = value!; + // setState(() {}); + // } + // }, + cells: [ + DataCell(SkTextInput(textController: TextEditingController())), + DataCell(Text('${dessert.calories}')), + DataCell(Text(dessert.fat.toStringAsFixed(1))), + DataCell(Text('${dessert.carbs}')), + DataCell(Text(dessert.protein.toStringAsFixed(1))), + DataCell(Text('${dessert.sodium}')), + DataCell(Text(format.format(dessert.calcium / 100))), + DataCell(Text(format.format(dessert.iron / 100))), + ], + ); + } + + void selectAll(bool? checked) { + for (final dessert in _desserts) { + dessert.selected = checked ?? false; + } + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + if (!_initialized) { + _dessertsDataSource = DessertDataSource(context); + _asyncDessertsDataSource = DessertDataSourceAsync(); + _initialized = true; + _dessertsDataSource.addListener(() { + setState(() {}); + }); + } + } + + void _sort( + Comparable Function(Dessert d) getField, + int columnIndex, + bool ascending, + ) { + _dessertsDataSource.sort(getField, ascending); + setState(() { + _sortColumnIndex = columnIndex; + _sortAscending = ascending; + }); + } + + Widget getTableFromSelectedType() { + switch (getCurrentRouteOption(context)) { + case paginatedFixedRowsCols: + { + return getPaginatedDataTable(); + } + case asyncPaginatedFixedRowsCols: + { + return getAsyncPaginatedDataTable(); + } + default: + { + return getDataTable(); + } + } + } + + @override + void dispose() { + _dessertsDataSource.dispose(); + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16), + child: Column(children: [ + // _getParamsWidget(context), + Flexible( + fit: FlexFit.tight, + child: Theme( + data: ThemeData(dividerColor: Colors.grey[400]), + child: getTableFromSelectedType())) + ])); + } + + DataTable2 getDataTable() { + return DataTable2( + scrollController: _controller, + columnSpacing: 0, + bottomMargin: 20, + border: TableBorder.all(width: 1.0, color: AppTheme.dividerColor), + // headingRowColor: MaterialStateProperty.resolveWith( + // (states) => _fixedRows > 0 ? Colors.grey[200] : Colors.transparent), + // headingRowDecoration: BoxDecoration( + // gradient: LinearGradient( + // colors: [ + // Colors.grey[400]!, + // Colors.grey[200]!, + // ], + // begin: Alignment.topCenter, + // end: Alignment.bottomCenter, + // ), + // ), + fixedColumnsColor: Colors.grey[100], + // fixedCornerColor: Colors.grey[400], + minWidth: 1000, + fixedTopRows: _fixedRows, + fixedLeftColumns: _fixedCols, + // sortColumnIndex: _sortColumnIndex, + // sortAscending: _sortAscending, + // onSelectAll: (val) => setState(() => selectAll(val)), + columns: [ + DataColumn2( + label: const Text('过渡'), + onSort: (columnIndex, ascending) => + _sort((d) => d.name, columnIndex, ascending), + ), + DataColumn2( + label: const Text('名称'), + size: ColumnSize.S, + numeric: true, + onSort: (columnIndex, ascending) => + _sort((d) => d.calories, columnIndex, ascending), + ), + DataColumn2( + label: const Text('规格、型号及说明'), + size: ColumnSize.S, + numeric: true, + onSort: (columnIndex, ascending) => + _sort((d) => d.fat, columnIndex, ascending), + ), + DataColumn2( + label: const Text('单位'), + size: ColumnSize.S, + numeric: true, + onSort: (columnIndex, ascending) => + _sort((d) => d.carbs, columnIndex, ascending), + ), + DataColumn2( + label: const Text('数量'), + size: ColumnSize.S, + numeric: true, + onSort: (columnIndex, ascending) => + _sort((d) => d.protein, columnIndex, ascending), + ), + DataColumn2( + label: const Text('备注'), + size: ColumnSize.S, + numeric: true, + onSort: (columnIndex, ascending) => + _sort((d) => d.sodium, columnIndex, ascending), + ), + DataColumn2( + label: const Text('单价'), + size: ColumnSize.S, + numeric: true, + onSort: (columnIndex, ascending) => + _sort((d) => d.calcium, columnIndex, ascending), + ), + DataColumn2( + label: const Text('总价'), + size: ColumnSize.S, + numeric: true, + onSort: (columnIndex, ascending) => + _sort((d) => d.iron, columnIndex, ascending), + ), + ], + rows: List.generate( + _dataItems, (index) => _getRow(index, Colors.transparent))); + } + + Widget getPaginatedDataTable() { + return PaginatedDataTable2( + scrollController: _controller, + columnSpacing: 0, + horizontalMargin: 12, + border: TableBorder.all(width: 1.0, color: Colors.grey), + headingRowColor: MaterialStateProperty.resolveWith( + (states) => _fixedRows > 0 ? Colors.grey[200] : Colors.transparent), + fixedColumnsColor: Colors.grey[300], + fixedCornerColor: Colors.grey[400], + minWidth: 1000, + fixedTopRows: _fixedRows, + fixedLeftColumns: _fixedCols, + sortColumnIndex: _sortColumnIndex, + sortAscending: _sortAscending, + onSelectAll: (val) => setState(() => selectAll(val)), + columns: [ + DataColumn2( + label: const Text('Desert'), + size: ColumnSize.S, + onSort: (columnIndex, ascending) => + _sort((d) => d.name, columnIndex, ascending), + ), + DataColumn2( + label: const Text('Calories'), + size: ColumnSize.S, + numeric: true, + onSort: (columnIndex, ascending) => + _sort((d) => d.calories, columnIndex, ascending), + ), + DataColumn2( + label: const Text('Fat (gm)'), + size: ColumnSize.S, + numeric: true, + onSort: (columnIndex, ascending) => + _sort((d) => d.fat, columnIndex, ascending), + ), + DataColumn2( + label: const Text('Carbs (gm)'), + size: ColumnSize.S, + numeric: true, + onSort: (columnIndex, ascending) => + _sort((d) => d.carbs, columnIndex, ascending), + ), + DataColumn2( + label: const Text('Protein (gm)'), + size: ColumnSize.S, + numeric: true, + onSort: (columnIndex, ascending) => + _sort((d) => d.protein, columnIndex, ascending), + ), + DataColumn2( + label: const Text('Sodium (mg)'), + size: ColumnSize.S, + numeric: true, + onSort: (columnIndex, ascending) => + _sort((d) => d.sodium, columnIndex, ascending), + ), + DataColumn2( + label: const Text('Calcium (%)'), + size: ColumnSize.S, + numeric: true, + onSort: (columnIndex, ascending) => + _sort((d) => d.calcium, columnIndex, ascending), + ), + DataColumn2( + label: const Text('Iron (%)'), + size: ColumnSize.S, + numeric: true, + onSort: (columnIndex, ascending) => + _sort((d) => d.iron, columnIndex, ascending), + ), + ], + source: _dessertsDataSource); + } + + Widget getAsyncPaginatedDataTable() { + return AsyncPaginatedDataTable2( + scrollController: _controller, + columnSpacing: 0, + horizontalMargin: 12, + border: TableBorder.all(width: 1.0, color: Colors.grey), + headingRowColor: MaterialStateProperty.resolveWith( + (states) => _fixedRows > 0 ? Colors.grey[200] : Colors.transparent), + fixedColumnsColor: Colors.grey[300], + fixedCornerColor: Colors.grey[400], + minWidth: 1000, + fixedTopRows: _fixedRows, + fixedLeftColumns: _fixedCols, + sortColumnIndex: _sortColumnIndex, + sortAscending: _sortAscending, + onSelectAll: (val) => setState(() => selectAll(val)), + columns: [ + DataColumn2( + label: const Text('Desert'), + size: ColumnSize.S, + onSort: (columnIndex, ascending) => + _sort((d) => d.name, columnIndex, ascending), + ), + DataColumn2( + label: const Text('Calories'), + size: ColumnSize.S, + numeric: true, + onSort: (columnIndex, ascending) => + _sort((d) => d.calories, columnIndex, ascending), + ), + DataColumn2( + label: const Text('Fat (gm)'), + size: ColumnSize.S, + numeric: true, + onSort: (columnIndex, ascending) => + _sort((d) => d.fat, columnIndex, ascending), + ), + DataColumn2( + label: const Text('Carbs (gm)'), + size: ColumnSize.S, + numeric: true, + onSort: (columnIndex, ascending) => + _sort((d) => d.carbs, columnIndex, ascending), + ), + DataColumn2( + label: const Text('Protein (gm)'), + size: ColumnSize.S, + numeric: true, + onSort: (columnIndex, ascending) => + _sort((d) => d.protein, columnIndex, ascending), + ), + DataColumn2( + label: const Text('Sodium (mg)'), + size: ColumnSize.S, + numeric: true, + onSort: (columnIndex, ascending) => + _sort((d) => d.sodium, columnIndex, ascending), + ), + DataColumn2( + label: const Text('Calcium (%)'), + size: ColumnSize.S, + numeric: true, + onSort: (columnIndex, ascending) => + _sort((d) => d.calcium, columnIndex, ascending), + ), + DataColumn2( + label: const Text('Iron (%)'), + size: ColumnSize.S, + numeric: true, + onSort: (columnIndex, ascending) => + _sort((d) => d.iron, columnIndex, ascending), + ), + ], + source: _asyncDessertsDataSource); + } + + Theme _getParamsWidget(BuildContext context) { + const double col1 = 130; + const double col2 = 210; + return Theme( + data: blackSlider(context), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + height: 36, + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + SizedBox( + width: col1, child: Text('Fixed rows ($_fixedRows)')), + SizedBox( + width: col2, + child: Slider( + value: _fixedRows.toDouble(), + min: 0, + max: 10, + divisions: 10, + onChanged: (val) { + setState(() { + _fixedRows = val.toInt(); + }); + })) + ], + )), + SizedBox( + height: 36, + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + SizedBox( + width: col1, + child: Text('Fixed columns ($_fixedCols)')), + SizedBox( + width: col2, + child: Slider( + value: _fixedCols.toDouble(), + min: 0, + max: 10, + divisions: 10, + onChanged: (val) { + setState(() { + _fixedCols = val.toInt(); + }); + })) + ], + )), + SizedBox( + height: 36, + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + SizedBox( + width: col1, child: Text('Data items ($_dataItems)')), + SizedBox( + width: col2, + child: Slider( + value: _dataItems.toDouble(), + min: 0, + max: 30, + divisions: 10, + onChanged: (val) { + setState(() { + _dataItems = val.toInt(); + }); + })) + ], + )) + ], + )); + } +} + +List _desserts = [ + Dessert( + 'Frozen Yogurt', + 159, + 6.0, + 24, + 4.0, + 87, + 14, + 1, + ), + Dessert( + 'Ice Cream Sandwich', + 237, + 9.0, + 37, + 4.3, + 129, + 8, + 1, + ), + Dessert( + 'Eclair', + 262, + 16.0, + 24, + 6.0, + 337, + 6, + 7, + ), + Dessert( + 'Cupcake', + 305, + 3.7, + 67, + 4.3, + 413, + 3, + 8, + ), + Dessert( + 'Gingerbread', + 356, + 16.0, + 49, + 3.9, + 327, + 7, + 16, + ), + Dessert( + 'Jelly Bean', + 375, + 0.0, + 94, + 0.0, + 50, + 0, + 0, + ), + Dessert( + 'Lollipop', + 392, + 0.2, + 98, + 0.0, + 38, + 0, + 2, + ), + Dessert( + 'Honeycomb', + 408, + 3.2, + 87, + 6.5, + 562, + 0, + 45, + ), + Dessert( + 'Donut', + 452, + 25.0, + 51, + 4.9, + 326, + 2, + 22, + ), + Dessert( + 'Apple Pie', + 518, + 26.0, + 65, + 7.0, + 54, + 12, + 6, + ), + Dessert( + 'Frozen Yougurt with sugar', + 168, + 6.0, + 26, + 4.0, + 87, + 14, + 1, + ), + Dessert( + 'Ice Cream Sandwich with sugar', + 246, + 9.0, + 39, + 4.3, + 129, + 8, + 1, + ), + Dessert( + 'Eclair with sugar', + 271, + 16.0, + 26, + 6.0, + 337, + 6, + 7, + ), + Dessert( + 'Cupcake with sugar', + 314, + 3.7, + 69, + 4.3, + 413, + 3, + 8, + ), + Dessert( + 'Gingerbread with sugar', + 345, + 16.0, + 51, + 3.9, + 327, + 7, + 16, + ), + Dessert( + 'Jelly Bean with sugar', + 364, + 0.0, + 96, + 0.0, + 50, + 0, + 0, + ), + Dessert( + 'Lollipop with sugar', + 401, + 0.2, + 100, + 0.0, + 38, + 0, + 2, + ), + Dessert( + 'Honeycomd with sugar', + 417, + 3.2, + 89, + 6.5, + 562, + 0, + 45, + ), + Dessert( + 'Donut with sugar', + 461, + 25.0, + 53, + 4.9, + 326, + 2, + 22, + ), + Dessert( + 'Apple pie with sugar', + 527, + 26.0, + 67, + 7.0, + 54, + 12, + 6, + ), + Dessert( + 'Forzen yougurt with honey', + 223, + 6.0, + 36, + 4.0, + 87, + 14, + 1, + ), + Dessert( + 'Ice Cream Sandwich with honey', + 301, + 9.0, + 49, + 4.3, + 129, + 8, + 1, + ), + Dessert( + 'Eclair with honey', + 326, + 16.0, + 36, + 6.0, + 337, + 6, + 7, + ), + Dessert( + 'Cupcake with honey', + 369, + 3.7, + 79, + 4.3, + 413, + 3, + 8, + ), + Dessert( + 'Gignerbread with hone', + 420, + 16.0, + 61, + 3.9, + 327, + 7, + 16, + ), + Dessert( + 'Jelly Bean with honey', + 439, + 0.0, + 106, + 0.0, + 50, + 0, + 0, + ), + Dessert( + 'Lollipop with honey', + 456, + 0.2, + 110, + 0.0, + 38, + 0, + 2, + ), + Dessert( + 'Honeycomd with honey', + 472, + 3.2, + 99, + 6.5, + 562, + 0, + 45, + ), + Dessert( + 'Donut with honey', + 516, + 25.0, + 63, + 4.9, + 326, + 2, + 22, + ), + Dessert( + 'Apple pie with honey', + 582, + 26.0, + 77, + 7.0, + 54, + 12, + 6, + ), +]; diff --git a/lib/screens/sale_quotation/components/helper.dart b/lib/screens/sale_quotation/components/helper.dart new file mode 100644 index 0000000..c0af457 --- /dev/null +++ b/lib/screens/sale_quotation/components/helper.dart @@ -0,0 +1,176 @@ +import 'package:flutter/material.dart'; +import 'dart:math'; + +ThemeData blackSlider(BuildContext context) { + return Theme.of(context).copyWith( + sliderTheme: SliderThemeData( + rangeThumbShape: + const RectRangeSliderThumbShape(enabledThumbRadius: 8), + thumbShape: const RectSliderThumbShape(enabledThumbRadius: 8), + thumbColor: Colors.grey[800], + activeTrackColor: Colors.grey[700], + inactiveTrackColor: Colors.grey[400], + activeTickMarkColor: Colors.white, + inactiveTickMarkColor: Colors.white)); +} + +class RectRangeSliderThumbShape extends RangeSliderThumbShape { + const RectRangeSliderThumbShape({ + this.enabledThumbRadius = 10.0, + this.disabledThumbRadius, + this.elevation = 1.0, + this.pressedElevation = 6.0, + }); + + final double enabledThumbRadius; + + final double? disabledThumbRadius; + double get _disabledThumbRadius => disabledThumbRadius ?? enabledThumbRadius; + + final double elevation; + + final double pressedElevation; + + @override + Size getPreferredSize(bool isEnabled, bool isDiscrete) { + return Size.fromRadius( + isEnabled == true ? enabledThumbRadius : _disabledThumbRadius); + } + + @override + void paint( + PaintingContext context, + Offset center, { + required Animation activationAnimation, + required Animation enableAnimation, + bool isDiscrete = false, + bool isEnabled = false, + bool? isOnTop, + required SliderThemeData sliderTheme, + TextDirection? textDirection, + Thumb? thumb, + bool? isPressed, + }) { + assert(sliderTheme.showValueIndicator != null); + assert(sliderTheme.overlappingShapeStrokeColor != null); + + final Canvas canvas = context.canvas; + final Tween radiusTween = Tween( + begin: _disabledThumbRadius, + end: enabledThumbRadius, + ); + final ColorTween colorTween = ColorTween( + begin: sliderTheme.disabledThumbColor, + end: sliderTheme.thumbColor, + ); + final double radius = radiusTween.evaluate(enableAnimation); + final Tween elevationTween = Tween( + begin: elevation, + end: pressedElevation, + ); + + if (isOnTop ?? false) { + final Paint strokePaint = Paint() + ..color = sliderTheme.overlappingShapeStrokeColor! + ..strokeWidth = 1.0 + ..style = PaintingStyle.stroke; + canvas.drawRect( + Rect.fromCenter( + center: center, width: 2 * radius, height: 2 * radius), + strokePaint); + } + + final Color color = colorTween.evaluate(enableAnimation)!; + + final double evaluatedElevation = + isPressed! ? elevationTween.evaluate(activationAnimation) : elevation; + final Path shadowPath = Path() + ..addArc( + Rect.fromCenter( + center: center, width: 2 * radius, height: 2 * radius), + 0, + pi * 2); + canvas.drawShadow(shadowPath, Colors.black, evaluatedElevation, true); + + canvas.drawRect( + Rect.fromCenter(center: center, width: 2 * radius, height: 2 * radius), + Paint()..color = color, + ); + } +} + +class RectSliderThumbShape extends SliderComponentShape { + const RectSliderThumbShape({ + this.enabledThumbRadius = 10.0, + this.disabledThumbRadius, + this.elevation = 1.0, + this.pressedElevation = 6.0, + }); + + final double enabledThumbRadius; + + final double? disabledThumbRadius; + double get _disabledThumbRadius => disabledThumbRadius ?? enabledThumbRadius; + + final double elevation; + + final double pressedElevation; + + @override + Size getPreferredSize(bool isEnabled, bool isDiscrete) { + return Size.fromRadius( + isEnabled == true ? enabledThumbRadius : _disabledThumbRadius); + } + + @override + void paint( + PaintingContext context, + Offset center, { + required Animation activationAnimation, + required Animation enableAnimation, + required bool isDiscrete, + required TextPainter labelPainter, + required RenderBox parentBox, + required SliderThemeData sliderTheme, + required TextDirection textDirection, + required double value, + required double textScaleFactor, + required Size sizeWithOverflow, + }) { + assert(sliderTheme.disabledThumbColor != null); + assert(sliderTheme.thumbColor != null); + + final Canvas canvas = context.canvas; + final Tween radiusTween = Tween( + begin: _disabledThumbRadius, + end: enabledThumbRadius, + ); + final ColorTween colorTween = ColorTween( + begin: sliderTheme.disabledThumbColor, + end: sliderTheme.thumbColor, + ); + + final Color color = colorTween.evaluate(enableAnimation)!; + final double radius = radiusTween.evaluate(enableAnimation); + + final Tween elevationTween = Tween( + begin: elevation, + end: pressedElevation, + ); + + final double evaluatedElevation = + elevationTween.evaluate(activationAnimation); + final Path path = Path() + ..addArc( + Rect.fromCenter( + center: center, width: 2 * radius, height: 2 * radius), + 0, + pi * 2); + canvas.drawShadow(path, Colors.black, evaluatedElevation, true); + + canvas.drawRect( + Rect.fromCenter(center: center, width: 2 * radius, height: 2 * radius), + Paint()..color = color, + ); + } +} diff --git a/lib/screens/sale_quotation/components/nav_helper.dart b/lib/screens/sale_quotation/components/nav_helper.dart new file mode 100644 index 0000000..8eb5d90 --- /dev/null +++ b/lib/screens/sale_quotation/components/nav_helper.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; + +/// Route options are used to configure certain features of +/// the given example +String getCurrentRouteOption(BuildContext context) { + var isEmpty = ModalRoute.of(context) != null && + ModalRoute.of(context)!.settings.arguments != null && + ModalRoute.of(context)!.settings.arguments is String + ? ModalRoute.of(context)!.settings.arguments as String + : ''; + + return isEmpty; +} + +// Route options +const dflt = 'Default'; +const noData = 'No data'; +const autoRows = 'Auto rows'; +const showBordersWithZebraStripes = 'Borders with Zebra'; +const custPager = 'Custom pager'; +const defaultSorting = 'Default sorting'; +const selectAllPage = 'Select all at page'; +const rowTaps = 'Row Taps'; +const rowHeightOverrides = 'Row height overrides'; +const fixedColumnWidth = 'Fixed column width'; +const dataTable2 = 'DataTable2'; +const paginatedFixedRowsCols = 'PaginatedDataTable2'; +const asyncPaginatedFixedRowsCols = 'AsyncPaginatedDataTable2'; +const custArrows = 'Custom sort arrows'; +const asyncErrors = + "Errors/Retries"; // Async sample that emulates network error and allow retrying load operation +const goToLast = + "Start at last page"; // Used by async example, navigates to the very last page upon opening the screen +const rounded = 'Rounded style'; + +/// Configurations available to given example routes +const Map> routeOptions = { + '/datatable2': [ + dflt, + noData, + showBordersWithZebraStripes, + fixedColumnWidth, + rowTaps, + rowHeightOverrides, + custArrows, + rounded + ], + '/paginated2': [dflt, noData, autoRows, custPager, defaultSorting], + '/datatable2fixedmn': [ + dataTable2, + paginatedFixedRowsCols, + asyncPaginatedFixedRowsCols + ], + '/asyncpaginated2': [ + dflt, + noData, + selectAllPage, + autoRows, + asyncErrors, + goToLast, + custPager + ], +}; + +List? getOptionsForRoute(String route) { + if (!routeOptions.containsKey(route)) { + return null; + } + + return routeOptions[route]; +} diff --git a/lib/screens/sale_quotation/sale_quotation.dart b/lib/screens/sale_quotation/sale_quotation.dart new file mode 100644 index 0000000..12387a3 --- /dev/null +++ b/lib/screens/sale_quotation/sale_quotation.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_sticky_header/flutter_sticky_header.dart'; +import 'package:sk_base_mobile/screens/sale_quotation/components/data_table.dart'; +import 'package:sk_base_mobile/util/screen_adaper_util.dart'; +import 'package:sk_base_mobile/widgets/sk_appbar.dart'; + +class SaleQuotationPage extends StatelessWidget { + const SaleQuotationPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: const SkAppbar(title: '报价计算'), + body: buildBody(), + ); + } + + Widget buildBody() { + return DataTable2FixedNMDemo(); + // return SingleChildScrollView( + // child: Column( + // children: [Text('11')], + // ), + // ); + // return SliverStickyHeader.builder( + // builder: (context, state) => Container( + // height: 60.0, + // color: (state.isPinned ? Colors.pink : Colors.lightBlue) + // .withOpacity(1.0 - state.scrollPercentage), + // padding: EdgeInsets.symmetric(horizontal: 16.0), + // alignment: Alignment.centerLeft, + // child: Text( + // 'Header #1', + // style: const TextStyle(color: Colors.white), + // ), + // ), + // sliver: SliverList( + // delegate: SliverChildBuilderDelegate( + // (context, i) => ListTile( + // leading: CircleAvatar( + // child: Text('0'), + // ), + // title: Text('List tile #$i'), + // ), + // childCount: 4, + // ), + // ), + // ); + } +} diff --git a/lib/screens/workbench/workbench.dart b/lib/screens/workbench/workbench.dart index 9d78eb6..e47def3 100644 --- a/lib/screens/workbench/workbench.dart +++ b/lib/screens/workbench/workbench.dart @@ -1,118 +1,150 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; +import 'package:flutter_svg/svg.dart'; import 'package:get/get.dart'; -import 'package:sk_base_mobile/app_theme.dart'; 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'; -class WorkBenchModel { - final String title; - final String route; - WorkBenchModel({required this.title, required this.route}); -} - class WorkBenchPage extends StatelessWidget { WorkBenchPage({super.key}); - final List works = [ - WorkBenchModel(title: '库存', route: '/inventory'), - WorkBenchModel(title: '产品', route: '/product'), - WorkBenchModel(title: '合同', route: '/contract'), - WorkBenchModel(title: '人事', route: '/personnel'), - WorkBenchModel(title: '公车', route: '/finance'), - WorkBenchModel(title: '任务', route: '/task_manage'), - WorkBenchModel(title: '报表', route: '/report'), - ]; + final controller = Get.put(WorkBenchController()); @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - leading: const SizedBox(), - title: const Text( - '工作台', - ), - ), - body: SingleChildScrollView( - child: Column(children: [ - Container( - padding: EdgeInsets.all(ScreenAdaper.width(20)), - child: GridView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: works.length, - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: Get.width > 800 ? 5 : 3, - crossAxisSpacing: ScreenAdaper.width(20), - mainAxisSpacing: ScreenAdaper.height(20), - childAspectRatio: 1.0), - itemBuilder: (BuildContext context, int index) { - return buildCard(index); - }, + appBar: AppBar( + leading: const SizedBox(), + title: const Text( + '工作台', ), - ) - ])), + ), + body: buildList() + // Container( + // padding: EdgeInsets.all(ScreenAdaper.width(20)), + // child: GridView.builder( + // shrinkWrap: true, + // physics: const NeverScrollableScrollPhysics(), + // itemCount: works.length, + // gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + // crossAxisCount: Get.width > 800 ? 5 : 3, + // crossAxisSpacing: ScreenAdaper.width(20), + // mainAxisSpacing: ScreenAdaper.height(20), + // childAspectRatio: 1.0), + // itemBuilder: (BuildContext context, int index) { + // return buildCard(index); + // }, + // ), + // ) + + ); + } + + Widget buildList() { + return MasonryGridView.count( + padding: EdgeInsets.only(top: ScreenAdaper.width(20)), + crossAxisCount: Get.width > 800 ? 6 : 4, + itemCount: controller.menus.length, + itemBuilder: (context, index) { + return buildItem( + index, + ); + }, ); } - Widget buildCard(int index) { + Widget buildItem(int index) { return InkWell( - onTap: () { - final route = RouteConfig.getPages - .map((e) => e.name) - .firstWhereOrNull((name) => name == works[index].route); - if (route != null) { - Get.toNamed(works[index].route); - } else { - SnackBarUtil().info('功能待开发。'); - } - }, - child: Container( - clipBehavior: Clip.antiAlias, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - gradient: const LinearGradient( - begin: Alignment.centerLeft, - end: Alignment.centerRight, - colors: [AppTheme.primaryColorLight, AppTheme.primaryColor]), - boxShadow: [ - BoxShadow( - color: AppTheme.black.withOpacity(0.4), - offset: const Offset(0, 0), - blurRadius: 1, - spreadRadius: 1) - ], + onTap: () { + final route = RouteConfig.getPages + .map((e) => e.name) + .firstWhereOrNull((name) => name == controller.menus[index].route); + if (route != null) { + Get.toNamed(controller.menus[index].route); + } else { + SnackBarUtil().info('功能待开发。'); + } + }, + child: Container( + alignment: Alignment.center, + padding: EdgeInsets.only( + top: ScreenAdaper.height(20), bottom: ScreenAdaper.height(20)), + child: Column( + children: [ + SvgPicture.asset( + 'assets/icons/${controller.menus[index].icon}', + width: ScreenAdaper.width(100), + height: ScreenAdaper.width(100), ), - child: Stack( - children: [ - // Image.asset(works[index].icon), - Center( - child: Stack( - alignment: Alignment.center, - children: [ - Text( - works[index].title, - style: TextStyle( - letterSpacing: ScreenAdaper.width(10), - fontSize: ScreenAdaper.height(40), - fontWeight: FontWeight.bold, - foreground: Paint() - ..style = PaintingStyle.stroke - ..strokeWidth = 5 - ..color = Colors.black), - ), - Text( - works[index].title, - style: TextStyle( - letterSpacing: ScreenAdaper.width(10), - color: Colors.white, - fontSize: ScreenAdaper.height(40), - fontWeight: FontWeight.bold), - ), - ], - ), - ) - ], - ))); + SizedBox( + height: ScreenAdaper.height(10), + ), + Text(controller.menus[index].title) + ], + ), + ), + ); } + // Widget buildCard(int index) { + // return InkWell( + // onTap: () { + // final route = RouteConfig.getPages + // .map((e) => e.name) + // .firstWhereOrNull((name) => name == works[index].route); + // if (route != null) { + // Get.toNamed(works[index].route); + // } else { + // SnackBarUtil().info('功能待开发。'); + // } + // }, + // child: Container( + // clipBehavior: Clip.antiAlias, + // decoration: BoxDecoration( + // borderRadius: BorderRadius.circular(10), + // gradient: const LinearGradient( + // begin: Alignment.centerLeft, + // end: Alignment.centerRight, + // colors: [AppTheme.primaryColorLight, AppTheme.primaryColor]), + // boxShadow: [ + // BoxShadow( + // color: AppTheme.black.withOpacity(0.4), + // offset: const Offset(0, 0), + // blurRadius: 1, + // spreadRadius: 1) + // ], + // ), + // child: Stack( + // children: [ + // // Image.asset(works[index].icon), + // Center( + // child: Stack( + // alignment: Alignment.center, + // children: [ + // Text( + // works[index].title, + // style: TextStyle( + // letterSpacing: ScreenAdaper.width(10), + // fontSize: ScreenAdaper.height(40), + // fontWeight: FontWeight.bold, + // foreground: Paint() + // ..style = PaintingStyle.stroke + // ..strokeWidth = 5 + // ..color = Colors.black), + // ), + // Text( + // works[index].title, + // style: TextStyle( + // letterSpacing: ScreenAdaper.width(10), + // color: Colors.white, + // fontSize: ScreenAdaper.height(40), + // fontWeight: FontWeight.bold), + // ), + // ], + // ), + // ) + // ], + // ))); + // } } diff --git a/lib/screens/workbench/workbench_controller.dart b/lib/screens/workbench/workbench_controller.dart index 530ea08..fbc8537 100644 --- a/lib/screens/workbench/workbench_controller.dart +++ b/lib/screens/workbench/workbench_controller.dart @@ -1,9 +1,20 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:sk_base_mobile/models/workbench.model.dart'; class WorkBenchController extends GetxController { late final AnimationController animationController; - + final List menus = [ + WorkBenchModel(title: '库存', route: '/inventory', icon: 'inventory.svg'), + WorkBenchModel(title: '产品', route: '/product', icon: 'product.svg'), + WorkBenchModel(title: '合同', route: '/contract', icon: 'contract.svg'), + WorkBenchModel(title: '人事', route: '/hr', icon: 'hr.svg'), + WorkBenchModel(title: '公车', route: '/vehicle', icon: 'vehicle.svg'), + WorkBenchModel(title: '任务', route: '/task_manage', icon: 'task_manage.svg'), + WorkBenchModel(title: '报表', route: '/report', icon: 'report.svg'), + WorkBenchModel( + title: '报价计算', route: '/sale_quotation', icon: 'sale_quotation.svg'), + ]; @override void onClose() { animationController.dispose(); diff --git a/lib/util/modal.util.dart b/lib/util/modal.util.dart index a547900..7660244 100644 --- a/lib/util/modal.util.dart +++ b/lib/util/modal.util.dart @@ -2,7 +2,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:sk_base_mobile/util/screen_adaper_util.dart'; -import 'package:sk_base_mobile/widgets/core/zt_bottomsheet_picker.dart'; +import 'package:sk_base_mobile/widgets/core/sk_bottomsheet_picker.dart'; class ModalUtil { static Future showWarningDialog( @@ -55,7 +55,7 @@ class ModalUtil { showCupertinoModalPopup( context: Get.overlayContext!, builder: (BuildContext context) { - return ZtBottomSheetPicker( + return SkBottomSheetPicker( title: title, onChanged: onChanged, firstLevel: firstLevel, diff --git a/lib/widgets/core/zt_base_date_picker.dart b/lib/widgets/core/sk_base_date_picker.dart similarity index 98% rename from lib/widgets/core/zt_base_date_picker.dart rename to lib/widgets/core/sk_base_date_picker.dart index 8eecbff..d15def2 100644 --- a/lib/widgets/core/zt_base_date_picker.dart +++ b/lib/widgets/core/sk_base_date_picker.dart @@ -5,14 +5,14 @@ import 'package:sk_base_mobile/util/logger_util.dart'; import 'package:sk_base_mobile/util/screen_adaper_util.dart'; // ignore: must_be_immutable -class ZtBaseDatePicker extends StatelessWidget { +class SkBaseDatePicker extends StatelessWidget { final Function(String)? onDateTimeChanged; late final FixedExtentScrollController yearController; late final FixedExtentScrollController monthController; late final FixedExtentScrollController dayController; DateTime? initialDate = DateTime.now(); DateTime? endDate = DateTime.now(); - ZtBaseDatePicker( + SkBaseDatePicker( {super.key, this.onDateTimeChanged, this.initialDate, this.endDate}); @override diff --git a/lib/widgets/core/zt_bottomsheet_picker.dart b/lib/widgets/core/sk_bottomsheet_picker.dart similarity index 98% rename from lib/widgets/core/zt_bottomsheet_picker.dart rename to lib/widgets/core/sk_bottomsheet_picker.dart index 7309cb0..60c4443 100644 --- a/lib/widgets/core/zt_bottomsheet_picker.dart +++ b/lib/widgets/core/sk_bottomsheet_picker.dart @@ -4,7 +4,7 @@ import 'package:get/get.dart'; import 'package:sk_base_mobile/app_theme.dart'; import 'package:sk_base_mobile/util/screen_adaper_util.dart'; -class ZtBottomSheetPicker extends StatelessWidget { +class SkBottomSheetPicker extends StatelessWidget { final Function? onChanged; final FixedExtentScrollController firstLevelControlller = FixedExtentScrollController(); @@ -18,7 +18,7 @@ class ZtBottomSheetPicker extends StatelessWidget { final double? itemHeight; final double? popupHeight; final String title; - ZtBottomSheetPicker( + SkBottomSheetPicker( {super.key, this.title = '请选择', required this.onChanged, diff --git a/lib/widgets/core/zk_date_picker.dart b/lib/widgets/core/sk_date_picker.dart similarity index 97% rename from lib/widgets/core/zk_date_picker.dart rename to lib/widgets/core/sk_date_picker.dart index 47d823a..c03a55a 100644 --- a/lib/widgets/core/zk_date_picker.dart +++ b/lib/widgets/core/sk_date_picker.dart @@ -2,14 +2,14 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:sk_base_mobile/util/screen_adaper_util.dart'; -class ZtDatePicker extends StatelessWidget { +class SkDatePicker extends StatelessWidget { final TextEditingController textController; final VoidCallback? onClear; final Function? onDateSelected; final bool isRequired; final String labelText; - const ZtDatePicker({ + const SkDatePicker({ super.key, this.onClear, this.onDateSelected, diff --git a/lib/widgets/core/zt_number_input.dart b/lib/widgets/core/sk_number_input.dart similarity index 96% rename from lib/widgets/core/zt_number_input.dart rename to lib/widgets/core/sk_number_input.dart index 1d87d1c..1781362 100644 --- a/lib/widgets/core/zt_number_input.dart +++ b/lib/widgets/core/sk_number_input.dart @@ -3,7 +3,7 @@ import 'package:flutter/services.dart'; import 'package:sk_base_mobile/app_theme.dart'; import 'package:sk_base_mobile/util/screen_adaper_util.dart'; -class ZtNumberInput extends StatelessWidget { +class SkNumberInput extends StatelessWidget { final TextEditingController textController; final VoidCallback? onTap; final Function(String)? onChanged; @@ -11,7 +11,7 @@ class ZtNumberInput extends StatelessWidget { final bool isRequired; final String labelText; final String? hint; - const ZtNumberInput( + const SkNumberInput( {super.key, required this.textController, this.onTap, diff --git a/lib/widgets/core/zt_search_select.dart b/lib/widgets/core/sk_search_select.dart similarity index 98% rename from lib/widgets/core/zt_search_select.dart rename to lib/widgets/core/sk_search_select.dart index f381e6f..e0bf0d8 100644 --- a/lib/widgets/core/zt_search_select.dart +++ b/lib/widgets/core/sk_search_select.dart @@ -3,7 +3,7 @@ import 'package:flutter_typeahead/flutter_typeahead.dart'; import 'package:sk_base_mobile/app_theme.dart'; import 'package:sk_base_mobile/util/screen_adaper_util.dart'; -class ZtSearchSelect extends StatelessWidget { +class SkSearchSelect extends StatelessWidget { final TextEditingController textController; final Function? suggestionsCallback; final void Function(T)? onSelected; @@ -13,7 +13,7 @@ class ZtSearchSelect extends StatelessWidget { final EdgeInsetsGeometry? contentPadding; final bool isRequired; final Widget Function(BuildContext, T) itemBuilder; - const ZtSearchSelect( + const SkSearchSelect( {super.key, required this.textController, required this.itemBuilder, diff --git a/lib/widgets/core/zt_text_input.dart b/lib/widgets/core/sk_text_input.dart similarity index 96% rename from lib/widgets/core/zt_text_input.dart rename to lib/widgets/core/sk_text_input.dart index b544de0..86e78e4 100644 --- a/lib/widgets/core/zt_text_input.dart +++ b/lib/widgets/core/sk_text_input.dart @@ -2,14 +2,14 @@ import 'package:flutter/material.dart'; import 'package:sk_base_mobile/app_theme.dart'; import 'package:sk_base_mobile/util/screen_adaper_util.dart'; -class ZtTextInput extends StatelessWidget { +class SkTextInput extends StatelessWidget { final TextEditingController textController; final VoidCallback? onTap; final bool isRequired; final String labelText; final String? hint; final bool isTextArea; - const ZtTextInput({ + const SkTextInput({ super.key, required this.textController, this.onTap, diff --git a/lib/widgets/sk_appbar.dart b/lib/widgets/sk_appbar.dart new file mode 100644 index 0000000..235224c --- /dev/null +++ b/lib/widgets/sk_appbar.dart @@ -0,0 +1,14 @@ +import 'package:flutter/material.dart'; + +class SkAppbar extends StatelessWidget implements PreferredSizeWidget { + final String title; + const SkAppbar({super.key, required this.title}); + + @override + Widget build(BuildContext context) { + return AppBar(title: Text(title)); + } + + @override + Size get preferredSize => Size.fromHeight(kToolbarHeight); +} diff --git a/pubspec.lock b/pubspec.lock index 60a6d95..80834f4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -161,6 +161,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.6" + data_table_2: + dependency: "direct main" + description: + name: data_table_2 + sha256: e403de6d9a58dddf27700114b614ea8ea5aa8442d7fbdfbe8b3d11b0512e7a49 + url: "https://pub.dev" + source: hosted + version: "2.5.12" date_format: dependency: "direct main" description: @@ -366,6 +374,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.0" + flutter_sticky_header: + dependency: "direct main" + description: + name: flutter_sticky_header + sha256: "017f398fbb45a589e01491861ca20eb6570a763fd9f3888165a978e11248c709" + url: "https://pub.dev" + source: hosted + version: "0.6.5" + flutter_svg: + dependency: "direct main" + description: + name: flutter_svg + sha256: "7b4ca6cf3304575fe9c8ec64813c8d02ee41d2afe60bcfe0678bcb5375d596a2" + url: "https://pub.dev" + source: hosted + version: "2.0.10+1" flutter_test: dependency: "direct dev" description: flutter @@ -592,6 +616,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.8.3" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf + url: "https://pub.dev" + source: hosted + version: "1.0.1" path_provider: dependency: "direct main" description: @@ -957,6 +989,38 @@ packages: url: "https://pub.dev" source: hosted version: "4.3.3" + value_layout_builder: + dependency: transitive + description: + name: value_layout_builder + sha256: "98202ec1807e94ac72725b7f0d15027afde513c55c69ff3f41bcfccb950831bc" + url: "https://pub.dev" + source: hosted + version: "0.3.1" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: "32c3c684e02f9bc0afb0ae0aa653337a2fe022e8ab064bcd7ffda27a74e288e3" + url: "https://pub.dev" + source: hosted + version: "1.1.11+1" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: c86987475f162fadff579e7320c7ddda04cd2fdeffbe1129227a85d9ac9e03da + url: "https://pub.dev" + source: hosted + version: "1.1.11+1" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: "12faff3f73b1741a36ca7e31b292ddeb629af819ca9efe9953b70bd63fc8cd81" + url: "https://pub.dev" + source: hosted + version: "1.1.11+1" vector_math: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 06196df..416fa25 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -61,6 +61,9 @@ dependencies: image_gallery_saver: ^2.0.3 flutter_staggered_grid_view: ^0.7.0 flutter_native_splash: ^2.3.11 + flutter_svg: ^2.0.10+1 + flutter_sticky_header: ^0.6.5 + data_table_2: ^2.5.12 dev_dependencies: flutter_test: sdk: flutter