import 'dart:convert'; import 'dart:io'; import 'package:decimal/decimal.dart'; import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:math_expressions/math_expressions.dart'; import 'package:open_filex/open_filex.dart'; import 'package:path_provider/path_provider.dart'; import 'package:sk_base_mobile/apis/api.dart'; import 'package:sk_base_mobile/app_theme.dart'; import 'package:sk_base_mobile/config.dart'; import 'package:sk_base_mobile/constants/constants.dart'; import 'package:sk_base_mobile/models/base_search_more_controller.dart'; import 'package:sk_base_mobile/models/sale_quotaion_component.model.dart'; import 'package:sk_base_mobile/models/sale_quotaion_group.model.dart'; import 'package:sk_base_mobile/models/sale_quotation.model.dart'; import 'package:sk_base_mobile/models/sale_quotation_template.model.dart'; import 'package:sk_base_mobile/models/workbench.model.dart'; import 'package:sk_base_mobile/router/router.util.dart'; import 'package:sk_base_mobile/screens/sale_quotation/components/sale_quotation_group_search.dart'; import 'package:sk_base_mobile/services/dio.service.dart'; import 'package:sk_base_mobile/services/storage.service.dart'; import 'package:sk_base_mobile/util/common.util.dart'; import 'package:sk_base_mobile/util/loading_util.dart'; import 'package:sk_base_mobile/util/logger_util.dart'; import 'package:sk_base_mobile/util/snack_bar.util.dart'; import 'package:sk_base_mobile/widgets/core/sk_flat_button.dart'; import 'package:sk_base_mobile/widgets/core/sk_ink.dart'; import 'package:sk_base_mobile/widgets/form_item/sk_multi_search_more.dart'; import 'package:sk_base_mobile/util/modal.util.dart'; import 'package:sk_base_mobile/util/screen_adaper_util.dart'; import 'package:pinyin/pinyin.dart'; import 'package:sk_base_mobile/widgets/form_item/sk_text_input.dart'; import 'package:sk_base_mobile/widgets/loading_indicator.dart'; class SaleQuotationController extends GetxController { static SaleQuotationController get to => Get.find(); final RxList editingcell = RxList([null, null, null]); final GlobalKey scaffoldKey = GlobalKey(); RxList groups = RxList([]); RxDouble totalCost = RxDouble(0.0); RxDouble totalPrice = RxDouble(0.0); RxBool isFormulaEditing = false.obs; RxString formula = '成本 * 1.3 / 0.864'.obs; RxList templates = RxList([]); final List menus = []; RxString templateName = '默认'.obs; RxnInt templateId = RxnInt(null); final downloadProgress = RxDouble(0.0); final RxBool loading = false.obs; @override void onReady() { init(); super.onReady(); } Future export() async { try { if (templateId.value == null) { SnackBarUtil().warning('请点击选中模板,若无模板请先创建'); return; } final dir = await getDownloadsDirectory(); if (dir != null) { String storagePath = dir.path; File file = File('$storagePath/$templateName.xls'); if (!file.existsSync()) { file.createSync(); } CancelToken token = CancelToken(); ModalUtil.alert( barrierDismissible: false, contentPadding: EdgeInsets.zero, showActions: false, content: // 进度条 Obx(() => Column( mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ Stack( children: [ SizedBox( height: ScreenAdaper.height(60), child: LinearProgressIndicator( value: downloadProgress.value, ), ), Positioned( left: 0, right: 0, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( '${downloadProgress.value * 100}%', textAlign: TextAlign.center, style: TextStyle( fontSize: ScreenAdaper.height(30)), ), const LoadingIndicator( color: AppTheme.nearlyBlack) ], )), ], ), SkFlatButton( onPressed: () { token.cancel('已取消下载'); Get.back(); }, textColor: AppTheme.nearlyBlack, color: AppTheme.nearlyWhite, buttonText: '取消', ) ], ))); try { await DioService.dio.download( '${Urls.saleQuotation}/export/${templateId.value}', file.path, cancelToken: token, onReceiveProgress: onReceiveProgress, options: Options( responseType: ResponseType.bytes, followRedirects: false, )); Get.back(); await OpenFilex.open(file.path); } catch (e) { Get.back(); SnackBarUtil().error((e as dynamic).error ?? '暂时无法下载,请稍后重试或联系管理员'); LoggerUtil().error(e); } } } catch (e) { SnackBarUtil().error('暂时无法下载,请稍后重试或联系管理员'); } } /// 展示下载进度 void onReceiveProgress(num received, num total) { LoggerUtil().info(received); if (total != -1) { downloadProgress.value = Decimal.parse((received / total).toStringAsFixed(2)) .toDouble() .toPrecision(2); } } Future init() async { menus.addAll([ WorkBenchModel(title: '导出明细', icon: 'export.svg', onTap: export), // WorkBenchModel(title: '模板', icon: 'sale_quotation_template.svg'), WorkBenchModel( title: '使用教程', icon: 'usage_guide.svg', onTap: () { RouterUtil.toNamed(RouteConfig.saleQuotationGuide); }), WorkBenchModel(title: '配件管理', icon: 'product.svg'), WorkBenchModel(title: '分组管理', icon: 'sale_quotation_group.svg'), // WorkBenchModel(title: '计算公式', icon: 'sale_quotation_formula.svg'), ]); String? salesQuotation = StorageService.to.getString('salesQuotation'); if (salesQuotation != null) { SaleQuotationTemplateModel editTemplate = SaleQuotationTemplateModel.fromJson(jsonDecode(salesQuotation)); parseTemplateModel(editTemplate); } await getTemplates(); } void parseTemplateModel(SaleQuotationTemplateModel editTemplate) { groups.assignAll(editTemplate.template.data); templateName.value = editTemplate.name ?? ''; formula.value = editTemplate.template.formula; totalPrice.value = editTemplate.template.totalPrice?.toDouble() ?? 0.0; totalCost.value = editTemplate.template.totalCost?.toDouble() ?? 0.0; templateId.value = editTemplate.id; } /// 切换报价模板 void changeTemplate(SaleQuotationTemplateModel templateModel) { parseTemplateModel(templateModel); saveToLocal(); } /// 获取报价模板 Future getTemplates() async { final res = await Api.getSaleQuotationTemplate(); List newList = res.data!.items .map((e) => SaleQuotationTemplateModel.fromJson(e)) .toList(); templates.assignAll(newList); } /// 实时计算总数 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 unitPrice = Decimal.fromInt(0); Decimal amount = Decimal.fromInt(0); if (data.quantity != 0) { quantity = Decimal.parse('${data.quantity}'); } if (data.unitPrice != 0) { unitPrice = Decimal.parse('${data.unitPrice}'); } if (data.amount != 0) { amount = Decimal.parse('${data.amount}'); } // 入库一般是先输入单价和数量,然后计算单价 if (changedField != 'amount') { Decimal result = unitPrice * quantity; data.amount = result != Decimal.zero ? result.toDouble() : 0; } if (changedField == 'amount' && quantity != Decimal.zero) { Decimal result = (amount / quantity).toDecimal(scaleOnInfinitePrecision: 10); data.unitPrice = result != Decimal.zero ? result.toDouble() : 0.0; } else if (changedField != 'amount') { Decimal result = (unitPrice * quantity); data.amount = result != Decimal.zero ? result.toDouble() : 0; } return data; } /// 处理行数据变化 void afterRowChanges(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]); saveToLocal(); } /// 保存到本地持久化 Future saveToLocal() async { Map data = { 'name': templateName.value, 'template': { 'data': groups.map((e) => e.toJson()).toList(), 'totalCost': totalCost.value, 'totalPrice': totalPrice.value, 'formula': formula.value, } }; if (templateId.value != null) { data['id'] = templateId.value; } await StorageService.to.setString('salesQuotation', jsonEncode(data)); } /// 检查当前模板是否合法 bool checkIsValid() { if (groups.isEmpty) { SnackBarUtil().warning('至少要创建一个组'); return false; } return true; } /// 保存到数据库 Future saveToDatabase({isSaveAs = false}) async { if (groups.isEmpty) { SnackBarUtil().warning('至少要创建一个组'); return false; } try { await LoadingUtil.to.show(); if (templateId.value == null || isSaveAs) { await Api.createSaleQuotationTemplate({ 'name': templateName.value, 'template': { 'data': groups.toJson(), 'totalCost': totalCost.value, 'totalPrice': totalPrice.value, 'formula': formula.value } }); await SnackBarUtil().success('已生成新的模板'); await getTemplates(); changeTemplate(templates.first); } else { await Api.updateSaleQuotationTemplate(templateId.value!, { 'name': templateName.value, 'template': { 'data': groups.toJson(), 'totalCost': totalCost.value, 'totalPrice': totalPrice.value, 'formula': formula.value } }); await SnackBarUtil().success('已保存'); await getTemplates(); } } finally { await LoadingUtil.to.dismiss(); } return true; } /// 删除模板 Future deleteTemplate(int deleteId) async { await Api.deleteSaleQuotationTemplate(deleteId); SnackBarUtil().success('已删除'); } /// 添加分组 void addGroup() async { final controller = Get.put(GroupSearchMoreController()); // 选择组件 选择分组 ModalUtil.showGeneralDialog( content: SkMutilSearchMore( controller: controller, title: '请选择分组', enablePullUp: false, enablePullDown: true, onOk: (List indexes) { groups.addAll(controller.list.where((element) { return indexes.contains(controller.list.indexOf(element)); })); saveToLocal(); }, 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(); } /// 移除分组 void removeGroup(int index) { groups.removeAt(index); calculateTotal(); saveToLocal(); } void addItems(int groupIndex) async { final controller = Get.put(ItemSearchMoreController()); // 选择配件 ModalUtil.showGeneralDialog( content: SkMutilSearchMore( controller: controller, enablePullUp: false, title: '请选择配件产品', enablePullDown: true, isDialog: true, onOk: (List indexes) { groups[groupIndex] .items .addAll(controller.list.where((element) { return indexes.contains(controller.list.indexOf(element)); }).map((e) { e.quantity = 1; Decimal amount = Decimal.parse(e.unitPrice.toString()) * Decimal.fromInt(e.quantity); e.amount = amount.toDouble(); return e; })); calculateTotal(); saveToLocal(); }, leadingBuilder: (index) { return Container( padding: EdgeInsets.symmetric( horizontal: ScreenAdaper.width(5), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text( controller.list[index].name, style: TextStyle( fontSize: ScreenAdaper.height(30), fontWeight: FontWeight.w600), ), ], ), if (controller.list[index].unit != null) Row( children: [ Text( '单位:${controller.list[index].unit ?? ''}', textAlign: TextAlign.start, style: TextStyle( fontSize: ScreenAdaper.height(23)), ), ], ), if (controller.list[index].componentSpecification != null) Row( children: [ Text( '规格、型号及说明:${controller.list[index].componentSpecification ?? ''}', textAlign: TextAlign.start, style: TextStyle( fontSize: ScreenAdaper.height(23)), ), ], ), Row( children: [ Text( '单价:¥${controller.list[index].unitPrice}', textAlign: TextAlign.start, style: TextStyle( fontSize: ScreenAdaper.height(23)), ), ], ), if (controller.list[index].remark != null) Row( children: [ Text( '备注:${controller.list[index].remark ?? ''}', textAlign: TextAlign.start, style: TextStyle( fontSize: ScreenAdaper.height(23)), ), ], ), ])); }, ), width: Get.width - ScreenAdaper.width(50)) .then((value) => Get.delete()); calculateTotal(); } void removeItem(int groupIndex, int rowIndex) { groups[groupIndex].items.removeAt(rowIndex); calculateTotal(); } void clearWorkbench() { groups.value = []; totalCost.value = 0.0; totalPrice.value = 0.0; formula.value = '成本 * 1.3 / 0.864'; templateName.value = '默认'; templateId.value = null; saveToLocal(); } void openTemplateNameEditPopup( {String? title = '', bool isSaveAs = false, Function(String)? onConfirm, Function? onCancel}) { if (!checkIsValid()) { return; } final textController = TextEditingController(text: title); Get.dialog(AlertDialog( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(ScreenAdaper.sp(20)), ), contentPadding: EdgeInsets.only( top: ScreenAdaper.height(10), right: ScreenAdaper.height(20), left: ScreenAdaper.height(20)), content: Column(mainAxisSize: MainAxisSize.min, children: [ Row( mainAxisAlignment: MainAxisAlignment.end, children: [ SkInk( onTap: () { RouterUtil.back(); }, child: const Icon(Icons.close)) ], ), SkTextInput( height: ScreenAdaper.height(100), textController: textController, customLabel: true, labelText: '模板名称', ), Row( children: [ Expanded( child: TextButton( onPressed: () { RouterUtil.back(); }, child: const Text( '取消', style: TextStyle(color: AppTheme.nearlyBlack), ), ), ), Expanded( child: TextButton( onPressed: () async { if (textController.text.isEmpty) { SnackBarUtil().error('模板名不能为空'); return; } await RouterUtil.back(); onConfirm?.call(textController.text); // templateName.value = textController.text; // await RouterUtil.back(); // await saveToLocal(); // await saveToDatabase(isSaveAs: isSaveAs); // await getTemplates(); }, child: const Text('确定'), )) ], ) ]), )); } // /// 检查是否已经保存 // void checkIsSaved() { // if (templateId.value == null) { // return false; // } // } } class GroupSearchMoreController extends BaseSearchMoreController { @override Future> getData({bool isRefresh = false}) async { await Future.delayed(const Duration(seconds: 1)); final res = await Api.getSaleQuotationGroups(); List groups = res.data!.items .map((e) => SaleQuotationGroupModel.fromJson(e)) .toList(); List newList = groups .map((e) => SaleQuotationModel(name: e.name!, items: RxList([]))) .toList(); // List newList = [ // SaleQuotationModel(name: '中间过渡架电控部分', items: RxList([])), // SaleQuotationModel(name: '端头架电控部分', items: RxList([])), // SaleQuotationModel(name: '主阀部分', items: RxList([])), // SaleQuotationModel(name: '自动反冲洗过滤器部分', items: RxList([])), // SaleQuotationModel(name: '位移测量部分', items: RxList([])), // SaleQuotationModel(name: '压力检测部分', items: RxList([])), // SaleQuotationModel(name: '煤机定位部分', items: RxList([])), // SaleQuotationModel(name: '姿态检测部分', items: RxList([])), // ]; list.assignAll(newList .where((element) => PinyinHelper.getPinyin(element.name, separator: '') .contains(searchKey.value)) .toList()); return newList; } } class ItemSearchMoreController extends BaseSearchMoreController { @override Future> getData({bool isRefresh = false}) async { try { final res = await Api.getSaleQuotationComponents(); List componets = res.data!.items .map((e) => SaleQuotationComponentModel.fromJson(e)) .toList(); List newList = componets .map((e) => SaleQuotationItemModel( name: e.name!, unitPrice: e.unitPrice ?? 0.0, unit: e.unit?.label ?? '', componentSpecification: e.componentSpecification, remark: e.remark)) .toList(); // List newList = [ // SaleQuotationItemModel( // name: '矿用本安型支架控制器', unit: '台', componentSpecification: 'ZDYZ-Z', unitPrice: 4700), // SaleQuotationItemModel(name: '矿用本安型电磁阀驱动器', unitPrice: 1200), // SaleQuotationItemModel(name: '矿用隔爆兼本安型电源', unitPrice: 5700), // SaleQuotationItemModel(name: '矿用本安型隔离耦合器', unitPrice: 1200), // SaleQuotationItemModel(name: '钢丝编织橡胶护套连接器', unitPrice: 600), // SaleQuotationItemModel(name: '钢丝编织橡胶护套连接器', remark: '控制器-控制器', unitPrice: 400), // SaleQuotationItemModel(name: '钢丝编织橡胶护套连接器', remark: '控制器-驱动器', unitPrice: 500), // SaleQuotationItemModel( // name: '钢丝编织橡胶护套连接器', remark: '控制器-隔离耦合器', unitPrice: 2000), // SaleQuotationItemModel(name: '矿用本安型支架控制器', unitPrice: 4700), // SaleQuotationItemModel(name: '矿用本安型电磁阀驱动器', unitPrice: 1200), // SaleQuotationItemModel(name: '矿用隔爆兼本安型电源', unitPrice: 5700), // SaleQuotationItemModel( // name: '电液换向阀(10功能10接口)', remark: '中间过渡架主阀组', unitPrice: 13200), // SaleQuotationItemModel( // name: '电液换向阀(20功能20接口)', remark: '端头架主阀组', unitPrice: 26500), // SaleQuotationItemModel( // name: '自动反冲洗过滤装置', remark: '流量:900L/min,过滤精度25μm', unitPrice: 2000), // SaleQuotationItemModel(name: '全自动反冲洗过滤器电缆', remark: '控制器-自动反冲洗'), // SaleQuotationItemModel(name: '矿用本安型位移传感器'), // SaleQuotationItemModel(name: '矿用本安型压力传感器'), // SaleQuotationItemModel(name: '矿用本安型红外发射器'), // SaleQuotationItemModel(name: '矿用本安型LED信号灯'), // SaleQuotationItemModel(name: '倾角传感器'), // SaleQuotationItemModel(name: '各类安装附件'), // ]; list.assignAll(newList .where((element) => PinyinHelper.getPinyin(element.name, separator: '') .contains(searchKey.value)) .toList()); return newList; } catch (e) { return []; } } }