From 10145b3af7dd4e8d848b4f9c854e6a362b35f982 Mon Sep 17 00:00:00 2001 From: louis <869322496@qq.com> Date: Wed, 3 Apr 2024 13:45:34 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=94=B5=E8=A7=A3=E6=8E=A7=E5=AE=9E?= =?UTF-8?q?=E6=97=B6=E8=AE=A1=E7=AE=97=E5=8A=9F=E8=83=BD=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/config.dart | 8 +- lib/models/base_search_more.model.dart | 6 + ....dart => base_search_more_controller.dart} | 4 +- lib/models/sale_quotation.model.dart | 43 +- lib/screens/login/login.dart | 4 +- .../components/sale_quotation_drawer.dart | 41 + .../sale_quotation_group_search.dart | 3 - .../sale_quotation.controller.dart | 196 +++-- .../sale_quotation/sale_quotation.dart | 728 +++++++++++------- lib/widgets/core/sk_muti_search_more.dart | 191 +++++ lib/widgets/core/sk_number_input.dart | 70 +- lib/widgets/core/sk_single_search_more.dart | 4 +- lib/widgets/core/sk_text_input.dart | 51 +- lib/widgets/gradient_button.dart | 39 +- lib/widgets/sk_appbar.dart | 15 +- pubspec.lock | 8 + pubspec.yaml | 1 + 17 files changed, 1022 insertions(+), 390 deletions(-) create mode 100644 lib/models/base_search_more.model.dart rename lib/models/{base_search_more.dart => base_search_more_controller.dart} (84%) create mode 100644 lib/screens/sale_quotation/components/sale_quotation_drawer.dart create mode 100644 lib/widgets/core/sk_muti_search_more.dart diff --git a/lib/config.dart b/lib/config.dart index 02b0257..605fa68 100644 --- a/lib/config.dart +++ b/lib/config.dart @@ -2,11 +2,11 @@ // Global config 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://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://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 = "山矿通"; diff --git a/lib/models/base_search_more.model.dart b/lib/models/base_search_more.model.dart new file mode 100644 index 0000000..22c25f8 --- /dev/null +++ b/lib/models/base_search_more.model.dart @@ -0,0 +1,6 @@ +import 'package:get/get_rx/src/rx_types/rx_types.dart'; + +class BaseSearchMoreModel { + RxBool isExpanded = true.obs; + String? name; +} diff --git a/lib/models/base_search_more.dart b/lib/models/base_search_more_controller.dart similarity index 84% rename from lib/models/base_search_more.dart rename to lib/models/base_search_more_controller.dart index 011c70c..b16d35e 100644 --- a/lib/models/base_search_more.dart +++ b/lib/models/base_search_more_controller.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart'; import 'package:get/get_rx/src/rx_types/rx_types.dart'; import 'package:pull_to_refresh/pull_to_refresh.dart'; +import 'package:sk_base_mobile/models/base_search_more.model.dart'; -mixin BaseSearchMore { +mixin BaseSearchMoreController { RxList list = RxList([]); RxString searchKey = ''.obs; final searchBarTextConroller = TextEditingController(); @@ -10,6 +11,7 @@ mixin BaseSearchMore { int page = 1; int limit = 15; int total = 0; + RxList selectedIndex = RxList([]); getData({bool isRefresh = false}) {} Future onRefresh() async { diff --git a/lib/models/sale_quotation.model.dart b/lib/models/sale_quotation.model.dart index 13add1f..2b37b5e 100644 --- a/lib/models/sale_quotation.model.dart +++ b/lib/models/sale_quotation.model.dart @@ -1,22 +1,35 @@ import 'package:get/get_rx/src/rx_types/rx_types.dart'; +import 'package:sk_base_mobile/models/base_search_more.model.dart'; -class SaleQuotationModel { +class SaleQuotationModel extends BaseSearchMoreModel { final String name; - RxBool isExpanded = true.obs; RxList items; SaleQuotationModel({required this.name, required this.items}); + // 生成fromjson + SaleQuotationModel.fromJson(Map json) + : name = json['name'], + items = (json['items'] as List) + .map((e) => SaleQuotationItemModel.fromJson(e)) + .toList() + .obs; + // 生成tojson + Map toJson() => { + 'name': name, + 'items': items.map((e) => e.toJson()).toList(), + }; } -class SaleQuotationItemModel { +class SaleQuotationItemModel extends BaseSearchMoreModel { final String name; // 规格 final String? spec; final String? unit; final String? remark; // 成本 - final double cost; - final int quantity; - final double amount; + int cost; + int quantity; + int amount; + SaleQuotationItemModel( {required this.name, this.spec, @@ -25,4 +38,22 @@ class SaleQuotationItemModel { this.cost = 0, this.quantity = 0, this.amount = 0}); + SaleQuotationItemModel.fromJson(Map json) + : name = json['name'], + spec = json['spec'], + unit = json['unit'], + remark = json['remark'], + cost = json['cost'], + quantity = json['quantity'], + amount = json['amount']; + // 生成tojson + Map toJson() => { + 'name': name, + 'spec': spec, + 'unit': unit, + 'remark': remark, + 'cost': cost, + 'quantity': quantity, + 'amount': amount, + }; } diff --git a/lib/screens/login/login.dart b/lib/screens/login/login.dart index ae0eb83..1dc90e5 100644 --- a/lib/screens/login/login.dart +++ b/lib/screens/login/login.dart @@ -172,8 +172,8 @@ class LoginScreen extends StatelessWidget { Widget buildUserNameInput() { return TextFormField( - // 英文数字键盘 - obscureText: true, + // 纯英文数字键盘 + keyboardType: TextInputType.emailAddress, decoration: InputDecoration( prefixIcon: Icon( Icons.person_2_outlined, diff --git a/lib/screens/sale_quotation/components/sale_quotation_drawer.dart b/lib/screens/sale_quotation/components/sale_quotation_drawer.dart new file mode 100644 index 0000000..53e599f --- /dev/null +++ b/lib/screens/sale_quotation/components/sale_quotation_drawer.dart @@ -0,0 +1,41 @@ +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'; + +class SaleQuotationEndDrawer extends StatelessWidget { + const SaleQuotationEndDrawer({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: SkAppbar(title: '工具栏', hideLeading: true), + body: buildBody(), + ); + } + + Widget buildBody() { + return Column( + children: [ + ListTile( + title: Text('选择报价模板'), + onTap: () {}, + ), + Divider( + color: AppTheme.dividerColor, + height: 1, + ), + Spacer(), + GradientButton( + icon: Icon( + Icons.save, + color: AppTheme.nearlyWhite, + size: ScreenAdaper.height(40), + ), + buttonText: '保存为模板', + ) + ], + ); + } +} diff --git a/lib/screens/sale_quotation/components/sale_quotation_group_search.dart b/lib/screens/sale_quotation/components/sale_quotation_group_search.dart index 3e17990..d0c5c15 100644 --- a/lib/screens/sale_quotation/components/sale_quotation_group_search.dart +++ b/lib/screens/sale_quotation/components/sale_quotation_group_search.dart @@ -3,15 +3,12 @@ import 'dart:async'; import 'package:flutter/material.dart'; 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/constants/bg_color.dart'; import 'package:sk_base_mobile/models/sale_quotation.model.dart'; -import 'package:sk_base_mobile/models/user_info.model.dart'; import 'package:sk_base_mobile/util/debouncer.dart'; import 'package:sk_base_mobile/util/screen_adaper_util.dart'; import 'package:sk_base_mobile/widgets/empty.dart'; -import 'package:sk_base_mobile/widgets/fade_in_cache_image.dart'; class SaleQuotationGroupSearch extends StatelessWidget { final Function(SaleQuotationModel)? onSelected; diff --git a/lib/screens/sale_quotation/sale_quotation.controller.dart b/lib/screens/sale_quotation/sale_quotation.controller.dart index 33dd30a..286de44 100644 --- a/lib/screens/sale_quotation/sale_quotation.controller.dart +++ b/lib/screens/sale_quotation/sale_quotation.controller.dart @@ -1,8 +1,15 @@ +import 'dart:convert'; + +import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:sk_base_mobile/models/base_search_more.dart'; +import 'package:math_expressions/math_expressions.dart'; +import 'package:sk_base_mobile/models/base_search_more_controller.dart'; import 'package:sk_base_mobile/models/sale_quotation.model.dart'; import 'package:sk_base_mobile/screens/sale_quotation/components/sale_quotation_group_search.dart'; +import 'package:sk_base_mobile/services/storage.service.dart'; +import 'package:sk_base_mobile/util/snack_bar.util.dart'; +import 'package:sk_base_mobile/widgets/core/sk_muti_search_more.dart'; import 'package:sk_base_mobile/widgets/core/sk_single_search_more.dart'; import 'package:sk_base_mobile/util/modal.util.dart'; import 'package:sk_base_mobile/util/screen_adaper_util.dart'; @@ -11,6 +18,7 @@ import 'package:pinyin/pinyin.dart'; class SaleQuotationController extends GetxController { static SaleQuotationController get to => Get.find(); final RxList editingcell = RxList([null, null, null]); + final GlobalKey scaffoldKey = GlobalKey(); RxList products = RxList([ SaleQuotationItemModel(name: '矿用本安型支架控制器', unit: '台', spec: 'ZDYZ-Z'), SaleQuotationItemModel(name: '矿用本安型电磁阀驱动器'), @@ -34,90 +42,178 @@ class SaleQuotationController extends GetxController { SaleQuotationItemModel(name: '各类安装附件'), ]); RxList groups = RxList([]); - + RxInt totalCost = RxInt(0); + RxDouble totalPrice = RxDouble(0.0); + RxBool isFormulaEditing = false.obs; + RxString formula = '成本 * 1.3 / 0.864'.obs; @override void onReady() { + init(); super.onReady(); } - Future addGroup() async { + Future init() async { + String? salesQuotation = StorageService.to.getString('salesQuotation'); + if (salesQuotation != null) { + groups.assignAll((jsonDecode(salesQuotation) as List) + .map((e) => SaleQuotationModel.fromJson(e)) + .toList()); + calculateTotal(); + } + } + + void calculateTotal({String? newFormula}) { + //计算groups中所有items中的amout总和 + totalCost.value = groups.fold(0, (previousValue, element) { + return previousValue + + element.items.fold(0, (previousValue, element) { + return previousValue + element.amount; + }); + }); + Parser p = Parser(); + Expression exp = p.parse(newFormula ?? formula.value); + Variable x = Variable('成本'); + ContextModel cm = ContextModel()..bindVariable(x, Number(totalCost.value)); + double eval = exp.evaluate(EvaluationType.REAL, cm); + totalPrice.value = eval.toPrecision(2); + } + + /// 实时计算数量,单价,总价之间的关系 + SaleQuotationItemModel calculateRow( + SaleQuotationItemModel data, String changedField) { + Decimal quantity = Decimal.fromInt(0); + Decimal cost = Decimal.fromInt(0); + Decimal amount = Decimal.fromInt(0); + if (data.quantity != 0) { + quantity = Decimal.parse('${data.quantity}'); + } + if (data.cost != 0) { + cost = Decimal.parse('${data.cost}'); + } + if (data.amount != 0) { + amount = Decimal.parse('${data.amount}'); + } + // 入库一般是先输入单价和数量,然后计算单价 + if (changedField != 'amount') { + Decimal result = cost * quantity; + data.amount = result != Decimal.zero ? result.toBigInt().toInt() : 0; + } + + if (changedField == 'amount' && quantity != Decimal.zero) { + Decimal result = + (amount / quantity).toDecimal(scaleOnInfinitePrecision: 10); + data.cost = result != Decimal.zero ? result.toBigInt().toInt() : 0; + } else if (changedField != 'amount') { + Decimal result = (cost * quantity); + data.amount = result != Decimal.zero ? result.toBigInt().toInt() : 0; + } + return data; + } + + void saveChanges(int groupIndex, int rowIndex, SaleQuotationItemModel data, + String changedField) { + data = calculateRow(data, changedField); + groups[groupIndex].items[rowIndex] = data; + calculateTotal(); + stopEditing(); + } + + void stopEditing() { + editingcell.assignAll([null, null, null]); + save(); + } + + Future save() async { + await StorageService.to + .setString('salesQuotation', jsonEncode(groups.toJson())); + // SnackBarUtil().success('已保存到本地'); + } + + void addGroup() async { final controller = Get.put(GroupSearchMoreController()); // 选择组件 选择分组 ModalUtil.showGeneralDialog( - content: SkSingleSearchMore( + content: SkMutilSearchMore( controller: controller, enablePullUp: false, enablePullDown: true, - itemBuilder: (_, index) { - return InkWell( - onTap: () { - Get.back(); - groups.add(controller.list[index]); - }, - child: Container( - padding: EdgeInsets.symmetric( - horizontal: ScreenAdaper.width(5), - vertical: ScreenAdaper.height(10)), - child: Row( - children: [ - Text( - controller.list[index].name, - style: TextStyle(fontSize: ScreenAdaper.height(25)), - ), - ], + onOk: (List indexes) { + groups.addAll(controller.list.where((element) { + return indexes.contains(controller.list.indexOf(element)); + })); + save(); + }, + leadingBuilder: (index) { + return Container( + padding: EdgeInsets.symmetric( + horizontal: ScreenAdaper.width(5), + vertical: ScreenAdaper.height(10)), + child: Row( + children: [ + Text( + controller.list[index].name, + style: TextStyle(fontSize: ScreenAdaper.height(30)), ), - )); + ], + ), + ); }, ), width: Get.width - ScreenAdaper.width(50)) .then((value) => Get.delete()); + calculateTotal(); } - Future removeGroup(int index) async { + void removeGroup(int index) { groups.removeAt(index); + calculateTotal(); + save(); } - Future addItems(int groupIndex) async { + void addItems(int groupIndex) async { final controller = Get.put(ItemSearchMoreController()); // 选择产品 ModalUtil.showGeneralDialog( - content: SkSingleSearchMore( + content: SkMutilSearchMore( controller: controller, enablePullUp: false, enablePullDown: true, isDialog: true, - itemBuilder: (_, index) { - return InkWell( - onTap: () { - Get.back(); - groups[groupIndex].items.add(controller.list[index]); - }, - child: Container( - padding: EdgeInsets.symmetric( - horizontal: ScreenAdaper.width(5), - vertical: ScreenAdaper.height(10)), - child: Row( - children: [ - Text( - controller.list[index].name, - style: TextStyle(fontSize: ScreenAdaper.height(25)), - ), - ], - ), - )); + onOk: (List indexes) { + groups[groupIndex].items.addAll(products.where((element) { + return indexes.contains(products.indexOf(element)); + })); + save(); + }, + leadingBuilder: (index) { + return Container( + padding: EdgeInsets.symmetric( + horizontal: ScreenAdaper.width(5), + vertical: ScreenAdaper.height(10)), + child: Row( + children: [ + Text( + controller.list[index].name, + style: TextStyle(fontSize: ScreenAdaper.height(25)), + ), + ], + ), + ); }, - // onSelected: (SaleQuotationItemModel info) { - // Get.back(); - // // groups.add(info); - // }, ), width: Get.width - ScreenAdaper.width(50)) .then((value) => Get.delete()); + calculateTotal(); + } + + void removeItem(int groupIndex, int rowIndex) { + groups[groupIndex].items.removeAt(rowIndex); + calculateTotal(); } } class GroupSearchMoreController extends GetxController - with BaseSearchMore { + with BaseSearchMoreController { @override Future> getData({bool isRefresh = false}) async { List newList = [ @@ -139,7 +235,7 @@ class GroupSearchMoreController extends GetxController } class ItemSearchMoreController extends GetxController - with BaseSearchMore { + with BaseSearchMoreController { @override Future> getData({bool isRefresh = false}) async { List newList = [ diff --git a/lib/screens/sale_quotation/sale_quotation.dart b/lib/screens/sale_quotation/sale_quotation.dart index cf0e97d..c45e52c 100644 --- a/lib/screens/sale_quotation/sale_quotation.dart +++ b/lib/screens/sale_quotation/sale_quotation.dart @@ -1,82 +1,228 @@ +import 'dart:ffi'; + import 'package:collection/collection.dart'; -import 'package:date_format/date_format.dart'; import 'package:flutter/material.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:get/get.dart'; import 'package:sk_base_mobile/app_theme.dart'; import 'package:sk_base_mobile/models/sale_quotation.model.dart'; +import 'package:sk_base_mobile/screens/sale_quotation/components/sale_quotation_drawer.dart'; import 'package:sk_base_mobile/screens/sale_quotation/sale_quotation.controller.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_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: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}); final controller = Get.put(SaleQuotationController()); - final quantityWidth = 55.0; - final unitPriceWidth = 80.0; - final amountWidth = 85.0; + final quantityWidth = 140.0; + final unitPriceWidth = 140.0; + final amountWidth = 140.0; final headerTitleStyle = TextStyle( fontSize: ScreenAdaper.height(30), fontWeight: FontWeight.w600, color: AppTheme.nearlyBlack); - final headerBgcolor = Color.fromARGB(255, 238, 238, 238); + final headerBgcolor = const Color.fromARGB(255, 238, 238, 238); @override Widget build(BuildContext context) { return Scaffold( - floatingActionButtonLocation: FloatingActionButtonLocation.endFloat, - floatingActionButton: FloatingActionButton( - onPressed: () { - controller.addGroup(); - }, - child: const Icon(Icons.add), + key: controller.scaffoldKey, + endDrawer: Drawer( + // 从右到左出现 + child: SaleQuotationEndDrawer(), ), appBar: SkAppbar( title: '报价计算', action: [ - TextButton( - onPressed: () { - controller.addGroup(); - }, - child: Row( - children: [ - Icon(Icons.save, color: AppTheme.white), - SizedBox( - width: ScreenAdaper.width(10), - ), - Text( - '保存', - style: TextStyle( - color: AppTheme.white, - fontSize: ScreenAdaper.height(25), - fontWeight: FontWeight.w600), - ) - ], - )) + IconButton( + onPressed: () { + controller.scaffoldKey.currentState?.openEndDrawer(); + }, + icon: const Icon(Icons.more_horiz_outlined, color: AppTheme.white), + ) ], ), body: SafeArea( bottom: ScreenAdaper.isLandspace() ? false : true, - child: Column( - children: [ - builderHeader(), - Expanded( - child: Obx(() => controller.groups.isEmpty - ? Empty(text: '请先添加分组') - : CustomScrollView( - slivers: controller.groups - .mapIndexed( - (index, e) => buildBody(e, index)) - .toList(), - ))), - ], + child: Stack(children: [ + Column( + children: [ + builderHeader(), + Expanded( + child: Obx(() => controller.groups.isEmpty + ? Empty(text: '请先添加分组') + : CustomScrollView( + slivers: controller.groups + .mapIndexed( + (index, e) => buildBody(index)) + .toList(), + ))), + SizedBox( + height: ScreenAdaper.height(100), + child: buildTotalCostRow(), + ), + SizedBox( + height: ScreenAdaper.height(100), + child: buildTotalSalesPriceRow(), + ) + ], + ), + // Positioned( + // bottom: ScreenAdaper.height(10), + // right: ScreenAdaper.height(10), + // child: IconButton( + // padding: EdgeInsets.all(ScreenAdaper.height(20)), + // style: ButtonStyle( + // backgroundColor: MaterialStateProperty.all( + // AppTheme.primaryColorLight)), + // onPressed: () { + // controller.addGroup(); + // }, + // icon: Icon( + // Icons.add, + // size: ScreenAdaper.height(40), + // ), + // )) + ]), + ), + ); + } + + Widget buildTotalSalesPriceRow() { + final formulaController = + TextEditingController(text: controller.formula.value); + return Container( + padding: EdgeInsets.symmetric( + horizontal: ScreenAdaper.width(20), + vertical: ScreenAdaper.height(10)), + decoration: BoxDecoration( + border: Border( + top: BorderSide(width: 1, color: Colors.grey[200]!), ), ), + child: Row( + children: [ + Text( + '合计', + style: TextStyle( + fontSize: ScreenAdaper.height(30), fontWeight: FontWeight.w600), + ), + Expanded( + child: Obx( + () => Container( + alignment: Alignment.centerLeft, + padding: + EdgeInsets.symmetric(horizontal: ScreenAdaper.width(10)), + height: ScreenAdaper.height(100), + width: ScreenAdaper.width(100), + child: controller.isFormulaEditing.value + ? SkTextInput( + hint: '', + isDense: true, + autoFocus: true, + onFieldSubmitted: (value) { + controller.formula.value = value; + controller.isFormulaEditing.value = false; + }, + validator: (String? value) { + if (value == null) { + return null; + } + try { + Parser p = Parser(); + Expression exp = p.parse(value); + Variable x = Variable('成本'); + ContextModel cm = ContextModel() + ..bindVariable( + x, Number(controller.totalCost.value)); + exp.evaluate(EvaluationType.REAL, cm); + return null; + } catch (e) { + return '公式错误'; + } + }, + onChanged: (String formula) { + try { + controller.calculateTotal(newFormula: formula); + } catch (e) {} + }, + onTapOutside: (String value) { + controller.formula.value = value; + controller.isFormulaEditing.value = false; + }, + contentPadding: EdgeInsets.symmetric( + horizontal: ScreenAdaper.width(20), + vertical: ScreenAdaper.height(10)), + textController: formulaController) + : InkWell( + onTap: () { + controller.isFormulaEditing.value = true; + }, + child: Text('(${controller.formula})', + style: TextStyle( + fontSize: ScreenAdaper.height(30), + color: AppTheme.grey)), + ), + ), + ), + ), + // IconButton( + // onPressed: () { + // controller.isFormulaEditing.value = + // !controller.isFormulaEditing.value; + // }, + // icon: Icon(Icons.edit, size: ScreenAdaper.height(30)), + // ), + Obx( + () => Text( + '¥${controller.totalPrice.value}', + textAlign: TextAlign.right, + style: TextStyle( + fontSize: ScreenAdaper.height(30), + fontWeight: FontWeight.w600), + ), + ) + ], + ), + ); + } + + Widget buildTotalCostRow() { + return Container( + padding: EdgeInsets.symmetric( + horizontal: ScreenAdaper.width(20), + vertical: ScreenAdaper.height(10)), + decoration: BoxDecoration( + border: Border( + top: BorderSide(width: 1, color: Colors.grey[200]!), + ), + ), + child: Row( + children: [ + Text( + '成本', + style: TextStyle( + fontSize: ScreenAdaper.height(30), fontWeight: FontWeight.w600), + ), + const Spacer(), + Obx( + () => Text( + '¥${controller.totalCost.value}', + style: TextStyle( + fontSize: ScreenAdaper.height(30), + fontWeight: FontWeight.w600), + ), + ) + ], + ), ); } @@ -100,7 +246,7 @@ class SaleQuotationPage extends StatelessWidget { const Spacer(), Container( alignment: Alignment.center, - width: quantityWidth, + width: ScreenAdaper.width(quantityWidth), // constraints: BoxConstraints(minWidth: quantityWidth), child: Text( '数量', @@ -109,8 +255,8 @@ class SaleQuotationPage extends StatelessWidget { ), Container( alignment: Alignment.center, - width: unitPriceWidth, - // constraints: BoxConstraints(minWidth: unitPriceWidth), + width: ScreenAdaper.width(unitPriceWidth), + // constraints: BoxConstraints(minWidth: ScreenAdaper.width(unitPriceWidth)), child: Text( '单价', style: headerTitleStyle, @@ -118,7 +264,7 @@ class SaleQuotationPage extends StatelessWidget { ), Container( alignment: Alignment.center, - width: amountWidth, + width: ScreenAdaper.width(amountWidth), child: Text( '总价', style: headerTitleStyle, @@ -128,259 +274,291 @@ class SaleQuotationPage extends StatelessWidget { )); } - Widget buildBody(SaleQuotationModel group, int groupIndex) { - final titleStyle = TextStyle( - fontSize: ScreenAdaper.height(30), - color: AppTheme.nearlyBlack, - fontWeight: FontWeight.w600); - final subTextStyle = - TextStyle(color: AppTheme.grey, fontSize: ScreenAdaper.height(30)); - + Widget buildBody(int groupIndex) { return SliverStickyHeader.builder( - builder: (context, state) => Container( - height: ScreenAdaper.height(80), - color: headerBgcolor.withOpacity(1.0 - state.scrollPercentage), - alignment: Alignment.centerLeft, - child: InkWell( - onTap: () { - controller.groups[groupIndex].isExpanded.value = - !controller.groups[groupIndex].isExpanded.value; - }, - child: Row( - children: [ - Obx(() => controller.groups[groupIndex].isExpanded.value - ? IconButton( - padding: EdgeInsets.zero, - onPressed: () { - controller.groups[groupIndex].isExpanded.value = - false; - }, - icon: const Icon( - Icons.expand_more, - color: AppTheme.nearlyBlack, - ), - ) - : IconButton( - padding: EdgeInsets.zero, - onPressed: () { - controller.groups[groupIndex].isExpanded.value = - true; - }, - icon: const Icon( - Icons.chevron_right, - color: AppTheme.nearlyBlack, - ), - )), - Text( - group.name, - style: titleStyle, - ), - const Spacer(), - IconButton( - onPressed: () { - ModalUtil.confirm( - title: '确定要删除此组吗?', - onConfirm: () { - controller.removeGroup(groupIndex); - }); - }, - icon: const Icon( - Icons.remove_circle_outline_outlined, - color: AppTheme.nearlyBlack, - )), - IconButton( - onPressed: () { - controller.addItems(groupIndex); - }, - icon: const Icon( - Icons.add_circle_outline, - color: AppTheme.nearlyBlack, - )), - ], - ))), + builder: (context, state) => buildGroupHeader(groupIndex, state), sliver: Obx(() => !controller.groups[groupIndex].isExpanded.value ? SliverList( delegate: SliverChildBuilderDelegate( - (context, i) => SizedBox(), + (context, i) => const SizedBox(), childCount: 0, )) : SliverList( delegate: SliverChildBuilderDelegate( - (context, int i) => Slidable( - key: UniqueKey(), - endActionPane: ActionPane( - extentRatio: 0.2, - motion: const DrawerMotion(), - children: [ - SlidableAction( - padding: EdgeInsets.zero, - onPressed: (_) { - controller.groups[groupIndex].items.removeAt(i); - }, - backgroundColor: AppTheme.dangerColor, - foregroundColor: Colors.white, - icon: Icons.delete, - label: '删除', - ), - ], - ), - child: Container( - padding: EdgeInsets.only( - left: ScreenAdaper.width(20), - top: ScreenAdaper.height(15), - bottom: ScreenAdaper.height(15), - ), - constraints: - BoxConstraints(minHeight: ScreenAdaper.height(100)), - decoration: BoxDecoration( - border: Border( - bottom: BorderSide(width: 1, color: Colors.grey[200]!), - ), - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Expanded( - child: Text( - group.items[i].name, - style: TextStyle( - fontWeight: FontWeight.w600, - fontSize: ScreenAdaper.height(30)), - ), - ), - ], - ), - if (group.items[i].spec != null) - Row( - children: [ - Text( - '规格、型号: ', - style: subTextStyle, - ), - Text( - group.items[i].spec ?? '', - style: subTextStyle, - ), - ], - ), - if (group.items[i].unit != null) - Row( - children: [ - Text( - '单位: ', - style: subTextStyle, - ), - Text( - group.items[i].unit ?? '', - style: subTextStyle, - ), - ], - ), - if (group.items[i].remark != null) - Row( - children: [ - Text( - '备注: ', - style: subTextStyle, - ), - Expanded( - child: Text( - '${group.items[i].remark ?? ''}${group.items[i].remark ?? ''}${controller.products[i].remark ?? ''}', - overflow: TextOverflow.ellipsis, - style: subTextStyle, - ), - ), - ], - ), - ], - ), - ), - VerticalDivider(), - buildEditCell( - Container( - alignment: Alignment.center, - width: quantityWidth, - child: Text( - '${controller.groups[groupIndex].items[i].quantity}', - style: TextStyle( - fontSize: ScreenAdaper.height(25)), - ), - ), - groupIndex: groupIndex, - rowIndex: i, - field: 'quantity', - inputWidth: quantityWidth, - value: controller - .groups[groupIndex].items[i].quantity), - buildEditCell( - Container( - alignment: Alignment.center, - width: unitPriceWidth, - child: Text( - '${controller.groups[groupIndex].items[i].cost}', - style: TextStyle( - fontSize: ScreenAdaper.height(25)), - ), - ), - groupIndex: groupIndex, - rowIndex: i, - field: 'cost', - inputWidth: unitPriceWidth, - value: controller.groups[groupIndex].items[i].cost), - buildEditCell( - Container( - alignment: Alignment.center, - width: amountWidth, - child: Text( - '${controller.groups[groupIndex].items[i].amount}', - style: TextStyle( - fontSize: ScreenAdaper.height(25)), - ), - ), - groupIndex: groupIndex, - rowIndex: i, - field: 'amount', - inputWidth: amountWidth, - value: - controller.groups[groupIndex].items[i].amount), - ], - ), - ), - ), + (context, int i) => buildRow(groupIndex, i), childCount: !controller.groups[groupIndex].isExpanded.value ? 0 - : group.items.length, + : controller.groups[groupIndex].items.length, ), )), ); } - Widget buildEditCell(Widget content, - {required int groupIndex, - required int rowIndex, - required String field, - required double inputWidth, - required dynamic value}) { + Widget buildGroupHeader(int groupIndex, SliverStickyHeaderState state) { + final titleStyle = TextStyle( + fontSize: ScreenAdaper.height(30), + color: AppTheme.nearlyBlack, + fontWeight: FontWeight.w600); + return Container( + height: ScreenAdaper.height(80), + color: headerBgcolor.withOpacity(1.0 - state.scrollPercentage), + alignment: Alignment.centerLeft, + child: InkWell( + onTap: () { + controller.groups[groupIndex].isExpanded.value = + !controller.groups[groupIndex].isExpanded.value; + }, + child: Row( + children: [ + Obx(() => controller.groups[groupIndex].isExpanded.value + ? IconButton( + padding: EdgeInsets.zero, + onPressed: () { + controller.groups[groupIndex].isExpanded.value = + false; + }, + icon: const Icon( + Icons.expand_more, + color: AppTheme.nearlyBlack, + ), + ) + : IconButton( + padding: EdgeInsets.zero, + onPressed: () { + controller.groups[groupIndex].isExpanded.value = true; + }, + icon: const Icon( + Icons.chevron_right, + color: AppTheme.nearlyBlack, + ), + )), + Text( + controller.groups[groupIndex].name, + style: titleStyle, + ), + const Spacer(), + IconButton( + onPressed: () { + ModalUtil.confirm( + title: '确定要删除此组吗?', + onConfirm: () { + controller.removeGroup(groupIndex); + }); + }, + icon: const Icon( + Icons.delete_outline, + color: AppTheme.nearlyBlack, + )), + IconButton( + onPressed: () { + controller.addItems(groupIndex); + }, + icon: const Icon( + Icons.add, + color: AppTheme.nearlyBlack, + )), + ], + ))); + } + + Widget buildRow(int groupIndex, int rowIndex) { + final subTextStyle = + TextStyle(color: AppTheme.grey, fontSize: ScreenAdaper.height(25)); + return Slidable( + key: UniqueKey(), + endActionPane: ActionPane( + extentRatio: 0.2, + motion: const DrawerMotion(), + children: [ + SlidableAction( + padding: EdgeInsets.zero, + onPressed: (_) { + controller.removeItem(groupIndex, rowIndex); + }, + backgroundColor: AppTheme.dangerColor, + foregroundColor: Colors.white, + icon: Icons.delete, + label: '删除', + ), + ], + ), + child: Container( + padding: EdgeInsets.only( + left: ScreenAdaper.width(20), + top: ScreenAdaper.height(15), + bottom: ScreenAdaper.height(15), + ), + constraints: BoxConstraints(minHeight: ScreenAdaper.height(100)), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide(width: 1, color: Colors.grey[200]!), + ), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + controller.groups[groupIndex].items[rowIndex].name, + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: ScreenAdaper.height(30)), + ), + ), + ], + ), + if (controller.groups[groupIndex].items[rowIndex].spec != + null) + Row( + children: [ + Text( + '规格、型号: ', + style: subTextStyle, + ), + Text( + controller.groups[groupIndex].items[rowIndex].spec ?? + '', + style: subTextStyle, + ), + ], + ), + if (controller.groups[groupIndex].items[rowIndex].unit != + null) + Row( + children: [ + Text( + '单位: ', + style: subTextStyle, + ), + Text( + controller.groups[groupIndex].items[rowIndex].unit ?? + '', + style: subTextStyle, + ), + ], + ), + if (controller.groups[groupIndex].items[rowIndex].remark != + null) + Row( + children: [ + Text( + '备注: ', + style: subTextStyle, + ), + Expanded( + child: Text( + '${controller.groups[groupIndex].items[rowIndex].remark ?? ''}${controller.groups[groupIndex].items[rowIndex].remark ?? ''}${controller.products[rowIndex].remark ?? ''}', + overflow: TextOverflow.ellipsis, + style: subTextStyle, + ), + ), + ], + ), + ], + ), + ), + VerticalDivider(), + buildEditCell( + Container( + alignment: Alignment.center, + width: ScreenAdaper.width(quantityWidth), + child: Text( + '${controller.groups[groupIndex].items[rowIndex].quantity}', + style: TextStyle(fontSize: ScreenAdaper.height(25)), + ), + ), + groupIndex: groupIndex, + rowIndex: rowIndex, + field: 'quantity', + inputWidth: ScreenAdaper.width(quantityWidth), + value: controller.groups[groupIndex].items[rowIndex].quantity, + func: (value) { + // 取消失去焦点,并且弹窗警告 + bool isValid = value > 0; + if (!isValid) { + SnackBarUtil().error('数量必须>0,若想删除产品0,请左滑'); + } + return isValid; + }), + buildEditCell( + Container( + alignment: Alignment.center, + width: ScreenAdaper.width(unitPriceWidth), + child: Text( + '${controller.groups[groupIndex].items[rowIndex].cost}', + style: TextStyle(fontSize: ScreenAdaper.height(25)), + ), + ), + groupIndex: groupIndex, + rowIndex: rowIndex, + field: 'cost', + inputWidth: ScreenAdaper.width(unitPriceWidth), + value: controller.groups[groupIndex].items[rowIndex].cost), + buildEditCell( + Container( + alignment: Alignment.center, + width: ScreenAdaper.width(amountWidth), + child: Text( + '${controller.groups[groupIndex].items[rowIndex].amount}', + style: TextStyle(fontSize: ScreenAdaper.height(25)), + ), + ), + groupIndex: groupIndex, + rowIndex: rowIndex, + field: 'amount', + inputWidth: ScreenAdaper.width(amountWidth), + value: controller.groups[groupIndex].items[rowIndex].amount), + ], + ), + ), + ); + } + + Widget buildEditCell( + Widget content, { + required int groupIndex, + required int rowIndex, + required String field, + required double inputWidth, + required dynamic value, + Function(dynamic)? func, + }) { return Obx(() => controller.editingcell[0] == groupIndex && controller.editingcell[1] == rowIndex && controller.editingcell[2] == field - ? Container( + ? SizedBox( width: inputWidth, - child: SkNumberInput( + child: SkNumberInput( hint: '', isDense: true, autoFocus: true, + validator: func, + onFieldSubmitted: (value) { + final editingData = + controller.groups[groupIndex].items[rowIndex].toJson(); + editingData[field] = value as T; + controller.saveChanges(groupIndex, rowIndex, + SaleQuotationItemModel.fromJson(editingData), field); + }, + onTapOutside: (dynamic value) { + final editingData = + controller.groups[groupIndex].items[rowIndex].toJson(); + editingData[field] = value as T; + controller.saveChanges(groupIndex, rowIndex, + SaleQuotationItemModel.fromJson(editingData), field); + }, contentPadding: EdgeInsets.symmetric( horizontal: ScreenAdaper.width(20), vertical: ScreenAdaper.height(10)), - textController: TextEditingController(text: '${value}')), + textController: + TextEditingController(text: '${value == 0 ? '' : value}')), ) : InkWell( onTap: () { diff --git a/lib/widgets/core/sk_muti_search_more.dart b/lib/widgets/core/sk_muti_search_more.dart new file mode 100644 index 0000000..bbb2328 --- /dev/null +++ b/lib/widgets/core/sk_muti_search_more.dart @@ -0,0 +1,191 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:pull_to_refresh/pull_to_refresh.dart'; +import 'package:sk_base_mobile/app_theme.dart'; +import 'package:sk_base_mobile/constants/bg_color.dart'; +import 'package:sk_base_mobile/models/base_search_more.model.dart'; +import 'package:sk_base_mobile/models/base_search_more_controller.dart'; +import 'package:sk_base_mobile/util/debouncer.dart'; +import 'package:sk_base_mobile/util/screen_adaper_util.dart'; +import 'package:sk_base_mobile/widgets/empty.dart'; +import 'package:sk_base_mobile/widgets/gradient_button.dart'; + +class SkMutilSearchMore extends StatelessWidget { + final Function(List)? onOk; + final Function(T)? beforeSelectedCheck; + // ignore: prefer_typing_uninitialized_variables + final BaseSearchMoreController controller; + final bool enablePullUp; + final bool enablePullDown; + final bool isDialog; + + final Function(int)? leadingBuilder; + SkMutilSearchMore( + {super.key, + required this.controller, + this.onOk, + this.isDialog = true, + this.beforeSelectedCheck, + this.leadingBuilder, + this.enablePullDown = true, + this.enablePullUp = true}); + final listTitleTextStyle = + TextStyle(fontSize: ScreenAdaper.height(20), fontWeight: FontWeight.w600); + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.symmetric( + horizontal: ScreenAdaper.width(20), + vertical: ScreenAdaper.height(20)), + child: Column( + children: [ + buildDialogHeader(), + SizedBox( + height: ScreenAdaper.height(defaultPadding) / 2, + ), + buildSearchBar(), + SizedBox( + height: ScreenAdaper.height(defaultPadding) / 2, + ), + Expanded(child: buildList()), + SizedBox( + height: ScreenAdaper.height(defaultPadding) / 2, + ), + GradientButton( + onPressed: () { + if (onOk != null) { + onOk!(controller.selectedIndex); + } + if (isDialog) { + Get.back(); + } + }, + buttonText: '确定', + ) + ], + )); + } + + Widget buildDialogHeader() { + return Container( + height: ScreenAdaper.height(60), + child: Stack( + children: [ + Center( + child: Text( + '请选择', + style: TextStyle( + fontSize: ScreenAdaper.height(30), + fontWeight: FontWeight.w600), + ), + ), + Positioned( + right: 0, + child: GestureDetector( + onTap: () { + Get.back(); + }, + child: Icon( + Icons.close, + size: ScreenAdaper.height(40), + ))) + ], + ), + ); + } + + Widget buildSearchBar() { + final doSearch = debouncer((String value) { + controller.searchKey.value = value; + controller.onRefresh(); + }, delayTime: 500); + + return Row( + children: [ + Expanded( + flex: 5, + child: TextField( + controller: controller.searchBarTextConroller, + onChanged: (value) => doSearch(value), + decoration: InputDecoration( + contentPadding: EdgeInsets.symmetric( + vertical: ScreenAdaper.height(10), + horizontal: ScreenAdaper.width(10)), + hintText: '名称', + floatingLabelBehavior: FloatingLabelBehavior.always, + prefixIcon: const Icon(Icons.search), + // 当searchBarController有值时不显示 + suffixIcon: Obx(() => controller.searchKey.value.isEmpty + ? const SizedBox() + : IconButton( + icon: const Icon(Icons.clear), + onPressed: () { + controller.searchKey.value = ''; + controller.searchBarTextConroller.clear(); + doSearch(''); + }, + )), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + ), + ))), + ], + ); + } + + Widget buildList() { + return Obx(() => SmartRefresher( + enablePullDown: enablePullDown, + enablePullUp: enablePullUp, + controller: controller.refreshController, + onLoading: controller.onLoading, + onRefresh: controller.onRefresh, + child: controller.refreshController.isLoading + ? const SizedBox() + : controller.list.isEmpty + ? const Center( + child: Empty(text: '暂无数据'), + ) + : ListView.separated( + separatorBuilder: (context, index) => const Divider( + color: AppTheme.dividerColor, + height: 1, + ), + itemCount: controller.list.length, + itemBuilder: (_, index) => buildItem(index)))); + } + + Widget buildItem(int index) { + return InkWell( + onTap: () { + controller.selectedIndex.contains(index) + ? controller.selectedIndex.remove(index) + : controller.selectedIndex.add(index); + }, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: ScreenAdaper.width(5), + vertical: ScreenAdaper.height(20)), + child: Row( + children: [ + Expanded( + child: leadingBuilder != null + ? leadingBuilder!(index) + : Text( + '-', + style: TextStyle(fontSize: ScreenAdaper.height(30)), + ), + ), + Obx(() => controller.selectedIndex.contains(index) + ? Icon( + Icons.check_circle, + color: Colors.green, + size: ScreenAdaper.height(40), + ) + : const SizedBox()) + ], + ), + ), + ); + } +} diff --git a/lib/widgets/core/sk_number_input.dart b/lib/widgets/core/sk_number_input.dart index f37daad..87c94be 100644 --- a/lib/widgets/core/sk_number_input.dart +++ b/lib/widgets/core/sk_number_input.dart @@ -2,36 +2,42 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:sk_base_mobile/app_theme.dart'; import 'package:sk_base_mobile/util/screen_adaper_util.dart'; +import 'package:sk_base_mobile/util/snack_bar.util.dart'; -class SkNumberInput extends StatefulWidget { +class SkNumberInput extends StatefulWidget { final TextEditingController textController; final Function(FocusNode)? onTap; final Function(String)? onChanged; + final Function(dynamic)? onTapOutside; + final Function(dynamic)? validator; + final bool isDense; final EdgeInsetsGeometry? contentPadding; final bool autoFocus; final bool isRequired; final String labelText; final String? hint; - - SkNumberInput({ - super.key, - required this.textController, - this.onTap, - this.onChanged, - this.hint, - this.isRequired = false, - this.labelText = '', - this.autoFocus = false, - this.contentPadding, - this.isDense = false, - }); + ValueChanged? onFieldSubmitted; + SkNumberInput( + {super.key, + required this.textController, + this.onTap, + this.onChanged, + this.hint, + this.onTapOutside, + this.isRequired = false, + this.labelText = '', + this.onFieldSubmitted, + this.autoFocus = false, + this.contentPadding, + this.isDense = false, + this.validator}); @override - State createState() => _SkNumberInputState(); + State createState() => _SkNumberInputState(); } -class _SkNumberInputState extends State { +class _SkNumberInputState extends State { late FocusNode focusNode = FocusNode(); @override void initState() { @@ -53,8 +59,38 @@ class _SkNumberInputState extends State { } }, controller: widget.textController, + onFieldSubmitted: (event) { + if (widget.onTapOutside != null) { + dynamic value; + if (T == double) { + value = double.tryParse(widget.textController.text); + + widget.onTapOutside!(value as T); + } else { + value = int.tryParse(widget.textController.text); + } + if (widget.validator != null && !widget.validator!(value)) { + return; + } + widget.onTapOutside!(value as T); + } + }, onTapOutside: (event) { - FocusScope.of(context).unfocus(); + if (widget.onTapOutside != null) { + dynamic value; + if (T == double) { + value = double.tryParse(widget.textController.text); + + widget.onTapOutside!(value as T); + } else { + value = int.tryParse(widget.textController.text); + } + if (widget.validator != null && !widget.validator!(value)) { + return; + } + widget.onTapOutside!(value as T); + FocusScope.of(context).unfocus(); + } }, onChanged: widget.onChanged ?? (_) {}, textAlign: TextAlign.center, diff --git a/lib/widgets/core/sk_single_search_more.dart b/lib/widgets/core/sk_single_search_more.dart index 49976bd..23a6210 100644 --- a/lib/widgets/core/sk_single_search_more.dart +++ b/lib/widgets/core/sk_single_search_more.dart @@ -3,7 +3,7 @@ import 'package:get/get.dart'; import 'package:pull_to_refresh/pull_to_refresh.dart'; import 'package:sk_base_mobile/app_theme.dart'; import 'package:sk_base_mobile/constants/bg_color.dart'; -import 'package:sk_base_mobile/models/base_search_more.dart'; +import 'package:sk_base_mobile/models/base_search_more_controller.dart'; import 'package:sk_base_mobile/util/debouncer.dart'; import 'package:sk_base_mobile/util/screen_adaper_util.dart'; import 'package:sk_base_mobile/widgets/empty.dart'; @@ -12,7 +12,7 @@ class SkSingleSearchMore extends StatelessWidget { final Function(T)? onSelected; final Function(T)? beforeSelectedCheck; // ignore: prefer_typing_uninitialized_variables - final BaseSearchMore controller; + final BaseSearchMoreController controller; final bool enablePullUp; final bool enablePullDown; final bool isDialog; diff --git a/lib/widgets/core/sk_text_input.dart b/lib/widgets/core/sk_text_input.dart index 245bdd4..304d534 100644 --- a/lib/widgets/core/sk_text_input.dart +++ b/lib/widgets/core/sk_text_input.dart @@ -10,20 +10,27 @@ class SkTextInput extends StatefulWidget { final String? hint; final bool isTextArea; final bool isDense; + final Function(String)? onTapOutside; + final Function(String)? onChanged; + final String? Function(String?)? validator; final EdgeInsetsGeometry? contentPadding; final bool autoFocus; - SkTextInput({ - super.key, - required this.textController, - this.onTap, - this.hint, - this.isRequired = false, - this.labelText = '', - this.isTextArea = false, - this.autoFocus = false, - this.contentPadding, - this.isDense = false, - }); + ValueChanged? onFieldSubmitted; + SkTextInput( + {super.key, + required this.textController, + this.onTap, + this.hint, + this.onFieldSubmitted, + this.isRequired = false, + this.onTapOutside, + this.labelText = '', + this.onChanged, + this.isTextArea = false, + this.autoFocus = false, + this.contentPadding, + this.isDense = false, + this.validator}); @override State createState() => _SkTextInputState(); @@ -31,6 +38,7 @@ class SkTextInput extends StatefulWidget { class _SkTextInputState extends State { late FocusNode focusNode = FocusNode(); + @override void initState() { super.initState(); @@ -46,16 +54,28 @@ class _SkTextInputState extends State { return TextFormField( focusNode: focusNode, controller: widget.textController, + onChanged: (String value) { + if (widget.onChanged != null) { + widget.onChanged!(value); + } + }, onTapOutside: (event) { - FocusScope.of(context).unfocus(); + if (widget.onTapOutside != null) { + widget.onTapOutside!(widget.textController.text); + FocusScope.of(context).unfocus(); + } }, maxLines: widget.isTextArea ? 2 : 1, // 添加这行代码 onTap: () { - if (widget.onTap == null) { + if (widget.onTap != null) { widget.onTap!(focusNode); } }, + onFieldSubmitted: widget.onFieldSubmitted, + autovalidateMode: AutovalidateMode.onUserInteraction, + validator: widget.validator, decoration: InputDecoration( + errorStyle: const TextStyle(fontSize: 0, height: 0.01), contentPadding: widget.contentPadding, isDense: widget.isDense, floatingLabelBehavior: FloatingLabelBehavior.always, @@ -75,8 +95,7 @@ class _SkTextInputState extends State { ), ]), focusedBorder: OutlineInputBorder( - borderSide: - const BorderSide(color: AppTheme.primaryColorLight, width: 2), + borderSide: BorderSide(color: AppTheme.primaryColorLight, width: 2), borderRadius: BorderRadius.circular(ScreenAdaper.sp(15))), hintText: widget.hint ?? '请输入', ), diff --git a/lib/widgets/gradient_button.dart b/lib/widgets/gradient_button.dart index 1077c33..952538d 100644 --- a/lib/widgets/gradient_button.dart +++ b/lib/widgets/gradient_button.dart @@ -8,10 +8,12 @@ class GradientButton extends StatelessWidget { final VoidCallback? onPressed; final bool isLoading; final String buttonText; + final Icon? icon; const GradientButton( {super.key, this.buttonText = TextEnum.createInventoryInOutBtnText, this.onPressed, + this.icon, this.isLoading = false}); @override Widget build(BuildContext context) { @@ -33,19 +35,30 @@ class GradientButton extends StatelessWidget { ), ), onPressed: onPressed ?? () => {}, - child: isLoading - ? LoadingAnimationWidget.fourRotatingDots( - color: AppTheme.nearlyWhite, - size: ScreenAdaper.height(40), - ) - : 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/sk_appbar.dart b/lib/widgets/sk_appbar.dart index b5c989a..b05f656 100644 --- a/lib/widgets/sk_appbar.dart +++ b/lib/widgets/sk_appbar.dart @@ -4,11 +4,24 @@ import 'package:sk_base_mobile/util/util.dart'; class SkAppbar extends StatelessWidget implements PreferredSizeWidget { final String title; final List? action; - const SkAppbar({super.key, required this.title, this.action}); + final bool hideLeading; + const SkAppbar( + {super.key, required this.title, this.action, this.hideLeading = false}); @override Widget build(BuildContext context) { return AppBar( + leading: hideLeading + ? const SizedBox() + : IconButton( + icon: Icon( + Icons.arrow_back_ios, + size: ScreenAdaper.height(40), + ), + onPressed: () { + Navigator.pop(context); + }, + ), title: Text( title, style: TextStyle(fontSize: ScreenAdaper.height(30)), diff --git a/pubspec.lock b/pubspec.lock index 4824d3a..5096f32 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -576,6 +576,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.0" + math_expressions: + dependency: "direct main" + description: + name: math_expressions + sha256: "3576593617c3870d75728a751f6ec6e606706d44e363f088ac394b5a28a98064" + url: "https://pub.dev" + source: hosted + version: "2.4.0" meta: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 6dab595..76bcb4f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -65,6 +65,7 @@ dependencies: flutter_sticky_header: ^0.6.5 flutter_slidable: ^3.1.0 pinyin: ^3.2.0 + math_expressions: ^2.4.0 dev_dependencies: flutter_test: sdk: flutter