import 'package:collection/collection.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/router/router.util.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/common.util.dart'; import 'package:sk_base_mobile/util/modal.util.dart'; import 'package:sk_base_mobile/util/screen_adaper_util.dart'; import 'package:sk_base_mobile/widgets/form_item/sk_number_input.dart'; import 'package:sk_base_mobile/widgets/form_item/sk_text_input.dart'; import 'package:sk_base_mobile/widgets/core/sk_appbar.dart'; import 'package:flutter_sticky_header/flutter_sticky_header.dart'; import 'package:math_expressions/math_expressions.dart'; class SaleQuotationPage extends StatelessWidget { SaleQuotationPage({super.key}); final controller = Get.put(SaleQuotationController()); final quantityWidth = 140.0; final unitPriceWidth = 140.0; final amountWidth = 140.0; final TextStyle? headerTitleStyle = TextStyle( fontSize: ScreenAdaper.height(30), fontWeight: FontWeight.w600, color: Theme.of(Get.context!).colorScheme.onPrimary); final headerBgcolor = AppTheme.appbarBgColor; @override Widget build(BuildContext context) { return Obx(() => Scaffold( key: controller.scaffoldKey, backgroundColor: AppTheme.nearlyWhite, endDrawer: Drawer( // 从右到左出现 child: SaleQuotationEndDrawer(), ), appBar: SkAppbar( onPop: () async { // 在这里判断是否允许用户返回 // 如果返回 true,用户可以返回 // 如果返回 false,用户不能返回 if (controller.templateName.value == '默认' && controller.groups.isNotEmpty) { await ModalUtil.alert( confirmText: '保存', cancelText: '继续退出', contentText: '是否保存当前模板?', onConfirm: () { controller.openTemplateNameEditPopup( onConfirm: (title) async { controller.templateName.value = title; await controller.saveToLocal(); await controller.saveToDatabase(); RouterUtil.back(); }); }, onCancel: () { RouterUtil.back(); }); } else { RouterUtil.back(); } }, title: '报价计算-${controller.templateName}', action: [ IconButton( onPressed: () { controller.scaffoldKey.currentState?.openEndDrawer(); }, icon: const Icon(Icons.more_horiz_outlined, color: AppTheme.white), ), ], ), body: SafeArea( bottom: ScreenAdaper.isLandspace() ? false : true, child: Stack(children: [ Column( children: [ builderHeader(), Expanded( child: Obx(() => controller.groups.isEmpty ? Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( '请右上角选择模板', style: TextStyle( fontSize: ScreenAdaper.height(30)), ), Text('或者左上角加号添加分组', style: TextStyle( fontSize: ScreenAdaper.height(30))) ], ) : CustomScrollView( slivers: controller.groups .mapIndexed( (index, e) => buildBody(index)) .toList(), ))), // 当键盘弹起时,不显示 SizedBox( height: ScreenAdaper.height(100), child: buildTotalCostRow(), ), SizedBox( height: ScreenAdaper.height(100), child: buildTotalSalesPriceRow(), ) ], ), ]), ), )); } 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), ), ) ], ), ); } Widget builderHeader() { return Container( decoration: BoxDecoration( color: headerBgcolor, border: Border( bottom: BorderSide(color: AppTheme.nearlyBlack.withOpacity(0.5)))), padding: EdgeInsets.only( left: ScreenAdaper.width(20), top: ScreenAdaper.height(10), bottom: ScreenAdaper.height(10)), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ GestureDetector( onTap: () { controller.addGroup(); }, child: Icon( Icons.add_circle_outline_outlined, size: ScreenAdaper.height(40), color: Theme.of(Get.context!).colorScheme.onPrimary, ), ), SizedBox( width: ScreenAdaper.width(5), ), Text( '名称', style: headerTitleStyle, ), const Spacer(), Container( alignment: Alignment.center, width: ScreenAdaper.width(quantityWidth), // constraints: BoxConstraints(minWidth: quantityWidth), child: Text( '数量', style: headerTitleStyle, ), ), Container( alignment: Alignment.center, width: ScreenAdaper.width(unitPriceWidth), // constraints: BoxConstraints(minWidth: ScreenAdaper.width(unitPriceWidth)), child: Text( '单价', style: headerTitleStyle, ), ), Container( alignment: Alignment.center, width: ScreenAdaper.width(amountWidth), child: Text( '总价', style: headerTitleStyle, ), ), ], )); } Widget buildBody(int groupIndex) { return SliverStickyHeader.builder( builder: (context, state) => buildGroupHeader(groupIndex, state), sliver: Obx(() => !controller.groups[groupIndex].isExpanded.value ? SliverList( delegate: SliverChildBuilderDelegate( (context, i) => const SizedBox(), childCount: 0, )) : SliverList( delegate: SliverChildBuilderDelegate( (context, int i) => buildRow(groupIndex, i), childCount: !controller.groups[groupIndex].isExpanded.value ? 0 : controller.groups[groupIndex].items.length, ), )), ); } Widget buildGroupHeader(int groupIndex, SliverStickyHeaderState state) { final titleStyle = TextStyle( fontSize: ScreenAdaper.height(30), color: AppTheme.nearlyBlack, fontWeight: FontWeight.w600); return Container( decoration: BoxDecoration( color: const Color.fromARGB(255, 235, 235, 235) .withOpacity(1.0 - state.scrollPercentage), border: const Border( top: BorderSide(width: 1, color: AppTheme.dividerColor), bottom: BorderSide(width: 1, color: AppTheme.dividerColor))), height: ScreenAdaper.height(80), 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.alert( contentText: '确定要删除此组吗?', 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(23)); 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] .componentSpecification != null) Row( children: [ // Text( // '规格、型号: ', // style: subTextStyle, // ), Text( controller.groups[groupIndex].items[rowIndex] .componentSpecification ?? '', 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 ?? '', overflow: TextOverflow.ellipsis, style: subTextStyle, ), ), ], ), ], ), ), const 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) > 0; // if (!isValid) { // SnackBarUtil().error('数量必须>0,若想删除产品0,请左滑'); // } return true; }), buildEditCell( Container( alignment: Alignment.center, width: ScreenAdaper.width(unitPriceWidth), child: Text( CommonUtil.toNumberWithoutZero(controller .groups[groupIndex].items[rowIndex].unitPrice), style: TextStyle(fontSize: ScreenAdaper.height(25)), ), ), groupIndex: groupIndex, rowIndex: rowIndex, field: 'unitPrice', inputWidth: ScreenAdaper.width(unitPriceWidth), value: controller.groups[groupIndex].items[rowIndex].unitPrice), buildEditCell( Container( alignment: Alignment.center, width: ScreenAdaper.width(amountWidth), child: Text( CommonUtil.toNumberWithoutZero( 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 ? SizedBox( width: inputWidth, child: SkNumberInput( hint: '', isDense: true, autoFocus: true, validator: func, onFieldSubmitted: (value) { final editingData = controller.groups[groupIndex].items[rowIndex].toJson(); editingData[field] = value; controller.afterRowChanges(groupIndex, rowIndex, SaleQuotationItemModel.fromJson(editingData), field); }, onTapOutside: (dynamic value) { value = value ?? 0; final editingData = controller.groups[groupIndex].items[rowIndex].toJson(); editingData[field] = value as T; controller.afterRowChanges(groupIndex, rowIndex, SaleQuotationItemModel.fromJson(editingData), field); }, contentPadding: EdgeInsets.symmetric( horizontal: ScreenAdaper.width(20), vertical: ScreenAdaper.height(10)), textController: TextEditingController( text: value == 0 ? '' : CommonUtil.toNumberWithoutZero(value))), ) : InkWell( onTap: () { controller.editingcell.value = [groupIndex, rowIndex, field]; }, child: content, )); } }