mobile_skt/lib/screens/sale_quotation/sale_quotation.dart

586 lines
21 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/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/form_item/sk_number_input.dart';
import 'package:sk_base_mobile/widgets/form_item/sk_text_input.dart';
import 'package:sk_base_mobile/widgets/empty.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 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<Widget>(
(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<Color>(
// 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<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;
if (!isValid) {
SnackBarUtil().error('数量必须>0,若想删除产品0请左滑');
}
return isValid;
}),
buildEditCell<int>(
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<int>(
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<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 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,
));
}
}