586 lines
21 KiB
Dart
586 lines
21 KiB
Dart
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/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,
|
||
));
|
||
}
|
||
}
|