mobile_skt/lib/screens/sale_quotation/sale_quotation.dart

622 lines
23 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<Widget>(
(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<int>(
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<double>(
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<double>(
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<T>(
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<T>(
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,
));
}
}