import 'dart:ffi'; 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/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 = 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 = const Color.fromARGB(255, 238, 238, 238); @override Widget build(BuildContext context) { return Scaffold( key: controller.scaffoldKey, endDrawer: const Drawer( // 从右到左出现 child: SaleQuotationEndDrawer(), ), appBar: SkAppbar( title: '报价计算', action: [ IconButton( onPressed: () { controller.scaffoldKey.currentState?.openEndDrawer(); }, icon: const Icon(Icons.more_horiz_outlined, color: AppTheme.white), ), Container( padding: EdgeInsets.symmetric( vertical: ScreenAdaper.height(10), horizontal: ScreenAdaper.width(20)), child: Row(mainAxisAlignment: MainAxisAlignment.end, children: [ GestureDetector( onTap: () { controller.addGroup(); }, child: Icon( Icons.add, size: ScreenAdaper.height(40), ), ) ]), ), ], ), body: SafeArea( bottom: ScreenAdaper.isLandspace() ? false : true, 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(220), // 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), ), ) ], ), ); } 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( children: [ 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( 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.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(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, ), ), ], ), ], ), ), 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; 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 ? 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 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 == 0 ? '' : value}')), ) : InkWell( onTap: () { controller.editingcell.value = [groupIndex, rowIndex, field]; }, child: content, )); } }