feat: sale quotation and support phone

This commit is contained in:
louis 2024-04-15 10:59:12 +08:00
parent 2cce68fe0c
commit 8e2737a5a4
22 changed files with 507 additions and 194 deletions

BIN
assets/icons/help_desk.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1713143223109" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7536" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48"><path d="M512.001023 2.193964c281.559068 0 509.805013 228.245945 509.805013 509.806036 0 281.557021-228.245945 509.805013-509.805013 509.805013S2.192941 793.557021 2.192941 512c0-281.560091 228.249015-509.806036 509.808082-509.806036zM313.40941 275.782516h-3.85581 3.85581z m-96.408555 11.56743c-7.706504 3.85888-15.422217 11.571523-15.422216 23.143047v385.611704c0 15.425287 11.5705 23.135884 26.994763 23.135883 50.126554-7.710597 161.958348-34.704337 262.217596 26.993741 0 0 3.85581 0 3.85581 3.85581V341.342543c-34.70536-26.99374-127.254011-96.408554-277.645953-53.992597z m586.137363 0c-146.531015-42.415957-242.934453 26.998857-277.639813 50.131671v412.608514s3.85888 0 3.85888-3.85581c100.255154-61.698077 212.082856-38.560147 266.070336-26.993741 11.5705 3.857857 26.99374-7.710597 26.99374-23.135883v-385.611704c-3.860927-11.571523-11.572547-19.283143-19.283143-23.143047z" fill="#E78800" p-id="7537"></path></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

View File

@ -22,37 +22,37 @@ class SaleQuotationModel extends BaseSearchMoreModel {
class SaleQuotationItemModel extends BaseSearchMoreModel { class SaleQuotationItemModel extends BaseSearchMoreModel {
final String name; final String name;
// //
final String? spec; final String? componentSpecification;
final String? unit; final String? unit;
final String? remark; final String? remark;
// //
num cost; num unitPrice;
int quantity; int quantity;
num amount; num amount;
SaleQuotationItemModel( SaleQuotationItemModel(
{required this.name, {required this.name,
this.spec, this.componentSpecification,
this.unit, this.unit,
this.remark, this.remark,
this.cost = 0, this.unitPrice = 0,
this.quantity = 0, this.quantity = 0,
this.amount = 0}); this.amount = 0});
SaleQuotationItemModel.fromJson(Map<String, dynamic> json) SaleQuotationItemModel.fromJson(Map<String, dynamic> json)
: name = json['name'], : name = json['name'],
spec = json['spec'], componentSpecification = json['componentSpecification'],
unit = json['unit'], unit = json['unit'],
remark = json['remark'], remark = json['remark'],
cost = json['cost'], unitPrice = json['unitPrice'],
quantity = json['quantity'], quantity = json['quantity'],
amount = json['amount']; amount = json['amount'];
// tojson // tojson
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
'name': name, 'name': name,
'spec': spec, 'componentSpecification': componentSpecification,
'unit': unit, 'unit': unit,
'remark': remark, 'remark': remark,
'cost': cost, 'unitPrice': unitPrice,
'quantity': quantity, 'quantity': quantity,
'amount': amount, 'amount': amount,
}; };

View File

@ -1,6 +1,8 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:sk_base_mobile/models/user_info.model.dart'; import 'package:sk_base_mobile/models/user_info.model.dart';
import 'package:sk_base_mobile/router/auth_middleware.dart'; import 'package:sk_base_mobile/router/auth_middleware.dart';
import 'package:sk_base_mobile/screens/guide/components/sale_quotation_guide.dart';
import 'package:sk_base_mobile/screens/guide/guide.dart';
import 'package:sk_base_mobile/screens/hr_manage/components/employee_detail.dart'; import 'package:sk_base_mobile/screens/hr_manage/components/employee_detail.dart';
import 'package:sk_base_mobile/screens/hr_manage/hr_manage.dart'; import 'package:sk_base_mobile/screens/hr_manage/hr_manage.dart';
import 'package:sk_base_mobile/screens/inventory/inventory.dart'; import 'package:sk_base_mobile/screens/inventory/inventory.dart';
@ -18,8 +20,18 @@ class RouteConfig {
static const String saleQuotation = '/workbench/sale_quotation'; static const String saleQuotation = '/workbench/sale_quotation';
static const String hrManage = '/workbench/hr_manage'; static const String hrManage = '/workbench/hr_manage';
static const String employeeDetail = '/employee_detail'; static const String employeeDetail = '/employee_detail';
static const String guide = '/guide';
static const String saleQuotationGuide = '/sale_quotation_guide';
static final List<GetPage> getPages = [ static final List<GetPage> getPages = [
GetPage(
name: saleQuotationGuide,
page: () => SaleQuotationGuide(),
),
GetPage(
name: guide,
page: () => GuidePage(),
),
GetPage( GetPage(
name: login, name: login,
page: () => LoginScreen(), page: () => LoginScreen(),

View File

@ -10,6 +10,7 @@ class RouterUtil {
RouteConfig.inventory, RouteConfig.inventory,
RouteConfig.saleQuotation, RouteConfig.saleQuotation,
]; ];
// static List<String> whiteList = [RouteConfig.saleQuotationGuide];
static Future<T?> toNamed<T>(String routeName, {arguments}) async { static Future<T?> toNamed<T>(String routeName, {arguments}) async {
// //
if (Get.context != null) { if (Get.context != null) {

View File

@ -0,0 +1,76 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:sk_base_mobile/app_theme.dart';
import 'package:sk_base_mobile/util/screen_adaper_util.dart';
import 'package:sk_base_mobile/widgets/core/sk_appbar.dart';
import 'package:sk_base_mobile/widgets/custom_contact_icon.dart';
class SaleQuotationGuide extends StatelessWidget {
final controller = Get.put(SaleQuotationGuideController());
SaleQuotationGuide({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppTheme.black,
appBar: SkAppbar(
title: '使用教程',
action: [
const CustomContactIcon(),
SizedBox(width: ScreenAdaper.width(20))
],
),
body: buildBody(),
);
}
Widget buildBody() {
return Stack(
alignment: AlignmentDirectional.bottomCenter,
children: [
Padding(
padding: EdgeInsets.symmetric(
vertical: ScreenAdaper.height(40),
horizontal: ScreenAdaper.width(20)),
child: PageView.builder(
controller: controller.pageController,
itemBuilder: (_, int index) => Container(
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage(
'assets/images/sale_quotation_guide${index + 1}.jpg'),
),
),
),
itemCount: 4,
onPageChanged: (index) {
controller.currentPage.value = index;
},
),
),
Obx(() => Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(
4,
(index) => Container(
margin: const EdgeInsets.all(4.0),
width: 10.0,
height: 10.0,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: controller.currentPage.value == index
? AppTheme.primaryColorLight
: Colors.grey,
),
),
),
)),
],
);
}
}
class SaleQuotationGuideController extends GetxController {
final currentPage = 0.obs;
final pageController = PageController();
}

View File

@ -0,0 +1,33 @@
import 'package:flutter/material.dart';
import 'package:sk_base_mobile/router/router.dart';
import 'package:sk_base_mobile/router/router.util.dart';
import 'package:sk_base_mobile/widgets/core/sk_ink.dart';
class GuideModel {
final String title;
final String route;
GuideModel({required this.title, required this.route});
}
class GuidePage extends StatelessWidget {
GuidePage({super.key});
final guides = [
GuideModel(title: 'Guide 1', route: '/guide/1'),
GuideModel(title: 'Guide 2', route: '/guide/2')
];
@override
Widget build(BuildContext context) {
return ListView(
children: guides.map((e) => buildItem(e)).toList(),
);
}
Widget buildItem(GuideModel model) {
return SkInk(
onTap: () {
RouterUtil.toNamed(RouteConfig.guide);
},
child: Text(model.title),
);
}
}

View File

@ -12,6 +12,7 @@ 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/util/snack_bar.util.dart';
import 'package:sk_base_mobile/widgets/core/sk_flat_button.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/core/sk_ink.dart';
import 'package:sk_base_mobile/widgets/empty.dart';
import 'package:sk_base_mobile/widgets/form_item/sk_text_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:sk_base_mobile/widgets/core/sk_appbar.dart';
@ -33,7 +34,7 @@ class SaleQuotationEndDrawer extends StatelessWidget {
Expanded( Expanded(
child: buildTemplatePicker(), child: buildTemplatePicker(),
), ),
// buildAction() buildAction()
], ],
); );
} }
@ -43,38 +44,72 @@ class SaleQuotationEndDrawer extends StatelessWidget {
children: [ children: [
Expanded( Expanded(
child: SkFlatButton( child: SkFlatButton(
color: AppTheme.dangerColor, color: Colors.green,
onPressed: () { onPressed: () async {
// ModalUtil.alert(
// title: '请注意',
// content: Text(
// '之前的模板保存了吗?不保存会丢失。',
// style: TextStyle(fontSize: ScreenAdaper.height(30)),
// ),
// onConfirm: () async {
controller.clearWorkbench(); controller.clearWorkbench();
RouterUtil.back(); RouterUtil.back();
// });
}, },
icon: Icon( icon: Icon(
Icons.delete, Icons.add,
color: AppTheme.nearlyWhite, color: AppTheme.nearlyWhite,
size: ScreenAdaper.height(40), size: ScreenAdaper.height(35),
), ),
buttonText: '清空工作区', buttonText: '新建模板',
)),
Expanded(
child: SkFlatButton(
color: AppTheme.nearlyWhite,
textColor: AppTheme.nearlyBlack,
onPressed: () async {
controller.openTemplateNameEditPopup(
title: '',
onConfirm: (name) async {
controller.templateName.value = name;
await controller.saveToLocal();
await controller.saveToDatabase(isSaveAs: true);
await controller.getTemplates();
});
},
icon: Icon(
Icons.save_as,
color: AppTheme.nearlyBlack,
size: ScreenAdaper.height(35),
),
buttonText: '另存为',
)), )),
Expanded( Expanded(
child: SkFlatButton( child: SkFlatButton(
onPressed: () async { onPressed: () async {
if (controller.templateName.value != '默认') { if (!controller.checkIsValid()) {
await controller.saveToDatabase(); return;
await RouterUtil.back(); }
} else { if (controller.templateName.value != '默认') {
templateNameDialog(); final isSuccessed = await controller.saveToDatabase();
if (isSuccessed) await RouterUtil.back();
} else {
controller.openTemplateNameEditPopup(onConfirm: (name) async {
controller.templateName.value = name;
await controller.saveToLocal();
await controller.saveToDatabase();
await controller.getTemplates();
});
} }
// final isSuccessed = await controller.saveToDatabase();
// if (isSuccessed) await RouterUtil.back();
}, },
icon: Icon( icon: Icon(
Icons.save, Icons.save,
color: AppTheme.nearlyWhite, color: AppTheme.nearlyWhite,
size: ScreenAdaper.height(40), size: ScreenAdaper.height(35),
), ),
buttonText: '保存模板', buttonText: '保存模板',
)) )),
], ],
); );
} }
@ -117,7 +152,7 @@ class SaleQuotationEndDrawer extends StatelessWidget {
Widget buildItem(int index) { Widget buildItem(int index) {
return SkInk( return SkInk(
onTap: controller.menus[index].onTap, onTap: controller.menus[index].onTap ?? () {},
child: Container( child: Container(
alignment: Alignment.center, alignment: Alignment.center,
padding: EdgeInsets.only( padding: EdgeInsets.only(
@ -160,14 +195,21 @@ class SaleQuotationEndDrawer extends StatelessWidget {
height: ScreenAdaper.height(10), height: ScreenAdaper.height(10),
), ),
Expanded( Expanded(
child: ListView( child: controller.templates.isEmpty
padding: ? Empty(
EdgeInsets.symmetric(horizontal: ScreenAdaper.height(20)), onTap: () {
children: controller.templates Get.back();
.mapIndexed((int index, element) => controller.addGroup();
buildTemplateItem(element, index)) },
.toList(), text: '暂无模板,点我开始创建')
)) : ListView(
padding: EdgeInsets.symmetric(
horizontal: ScreenAdaper.height(20)),
children: controller.templates
.mapIndexed((int index, element) =>
buildTemplateItem(element, index))
.toList(),
))
], ],
), ),
)); ));
@ -199,7 +241,14 @@ class SaleQuotationEndDrawer extends StatelessWidget {
)), )),
SkInk( SkInk(
onTap: () { onTap: () {
templateNameDialog(title: element.name); controller.openTemplateNameEditPopup(
title: element.name,
onConfirm: (name) async {
controller.templateName.value = name;
await controller.saveToLocal();
await controller.saveToDatabase();
await controller.getTemplates();
});
}, },
child: Icon( child: Icon(
Icons.edit, Icons.edit,
@ -234,67 +283,6 @@ class SaleQuotationEndDrawer extends StatelessWidget {
), ),
)); ));
} }
void templateNameDialog({String? title = ''}) {
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;
}
controller.templateName.value = textController.text;
await RouterUtil.back();
await controller.saveToLocal();
await controller.saveToDatabase();
await controller.getTemplates();
},
child: const Text('确定'),
))
],
)
]),
));
}
} }
// class SaleQuotationDrawerController extends GetxController{ // class SaleQuotationDrawerController extends GetxController{

View File

@ -18,16 +18,21 @@ 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.model.dart';
import 'package:sk_base_mobile/models/sale_quotation_template.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/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/screens/sale_quotation/components/sale_quotation_group_search.dart';
import 'package:sk_base_mobile/services/dio.service.dart'; import 'package:sk_base_mobile/services/dio.service.dart';
import 'package:sk_base_mobile/services/storage.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/logger_util.dart';
import 'package:sk_base_mobile/util/snack_bar.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_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/widgets/form_item/sk_multi_search_more.dart';
import 'package:sk_base_mobile/util/modal.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/util/screen_adaper_util.dart';
import 'package:pinyin/pinyin.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'; import 'package:sk_base_mobile/widgets/loading_indicator.dart';
class SaleQuotationController extends GetxController { class SaleQuotationController extends GetxController {
@ -44,6 +49,7 @@ class SaleQuotationController extends GetxController {
RxString templateName = '默认'.obs; RxString templateName = '默认'.obs;
RxnInt templateId = RxnInt(null); RxnInt templateId = RxnInt(null);
final downloadProgress = RxDouble(0.0); final downloadProgress = RxDouble(0.0);
final RxBool loading = false.obs;
@override @override
void onReady() { void onReady() {
init(); init();
@ -52,6 +58,10 @@ class SaleQuotationController extends GetxController {
Future export() async { Future export() async {
try { try {
if (templateId.value == null) {
SnackBarUtil().warning('请点击选中模板,若无模板请先创建');
return;
}
final dir = await getDownloadsDirectory(); final dir = await getDownloadsDirectory();
if (dir != null) { if (dir != null) {
String storagePath = dir.path; String storagePath = dir.path;
@ -144,9 +154,15 @@ class SaleQuotationController extends GetxController {
menus.addAll([ menus.addAll([
WorkBenchModel(title: '导出明细', icon: 'export.svg', onTap: export), WorkBenchModel(title: '导出明细', icon: 'export.svg', onTap: export),
// WorkBenchModel(title: '模板', icon: 'sale_quotation_template.svg'), // 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: 'product.svg'),
WorkBenchModel(title: '分组管理', icon: 'sale_quotation_group.svg'), WorkBenchModel(title: '分组管理', icon: 'sale_quotation_group.svg'),
WorkBenchModel(title: '计算公式', icon: 'sale_quotation_formula.svg'), // WorkBenchModel(title: '计算公式', icon: 'sale_quotation_formula.svg'),
]); ]);
String? salesQuotation = StorageService.to.getString('salesQuotation'); String? salesQuotation = StorageService.to.getString('salesQuotation');
if (salesQuotation != null) { if (salesQuotation != null) {
@ -202,29 +218,29 @@ class SaleQuotationController extends GetxController {
SaleQuotationItemModel calculateRow( SaleQuotationItemModel calculateRow(
SaleQuotationItemModel data, String changedField) { SaleQuotationItemModel data, String changedField) {
Decimal quantity = Decimal.fromInt(0); Decimal quantity = Decimal.fromInt(0);
Decimal cost = Decimal.fromInt(0); Decimal unitPrice = Decimal.fromInt(0);
Decimal amount = Decimal.fromInt(0); Decimal amount = Decimal.fromInt(0);
if (data.quantity != 0) { if (data.quantity != 0) {
quantity = Decimal.parse('${data.quantity}'); quantity = Decimal.parse('${data.quantity}');
} }
if (data.cost != 0) { if (data.unitPrice != 0) {
cost = Decimal.parse('${data.cost}'); unitPrice = Decimal.parse('${data.unitPrice}');
} }
if (data.amount != 0) { if (data.amount != 0) {
amount = Decimal.parse('${data.amount}'); amount = Decimal.parse('${data.amount}');
} }
// //
if (changedField != 'amount') { if (changedField != 'amount') {
Decimal result = cost * quantity; Decimal result = unitPrice * quantity;
data.amount = result != Decimal.zero ? result.toDouble() : 0; data.amount = result != Decimal.zero ? result.toDouble() : 0;
} }
if (changedField == 'amount' && quantity != Decimal.zero) { if (changedField == 'amount' && quantity != Decimal.zero) {
Decimal result = Decimal result =
(amount / quantity).toDecimal(scaleOnInfinitePrecision: 10); (amount / quantity).toDecimal(scaleOnInfinitePrecision: 10);
data.cost = result != Decimal.zero ? result.toDouble() : 0.0; data.unitPrice = result != Decimal.zero ? result.toDouble() : 0.0;
} else if (changedField != 'amount') { } else if (changedField != 'amount') {
Decimal result = (cost * quantity); Decimal result = (unitPrice * quantity);
data.amount = result != Decimal.zero ? result.toDouble() : 0; data.amount = result != Decimal.zero ? result.toDouble() : 0;
} }
return data; return data;
@ -253,7 +269,7 @@ class SaleQuotationController extends GetxController {
'data': groups.map((e) => e.toJson()).toList(), 'data': groups.map((e) => e.toJson()).toList(),
'totalCost': totalCost.value, 'totalCost': totalCost.value,
'totalPrice': totalPrice.value, 'totalPrice': totalPrice.value,
'formula': formula.value 'formula': formula.value,
} }
}; };
if (templateId.value != null) { if (templateId.value != null) {
@ -262,31 +278,52 @@ class SaleQuotationController extends GetxController {
await StorageService.to.setString('salesQuotation', jsonEncode(data)); await StorageService.to.setString('salesQuotation', jsonEncode(data));
} }
/// ///
Future<bool> saveToDatabase() async { bool checkIsValid() {
if (templateId.value == null) { if (groups.isEmpty) {
await Api.createSaleQuotationTemplate({ SnackBarUtil().warning('至少要创建一个组');
'name': templateName.value, return false;
'template': { }
'data': groups.toJson(), return true;
'totalCost': totalCost.value, }
'totalPrice': totalPrice.value,
'formula': formula.value ///
} Future<bool> saveToDatabase({isSaveAs = false}) async {
}); if (groups.isEmpty) {
} else { SnackBarUtil().warning('至少要创建一个组');
await Api.updateSaleQuotationTemplate(templateId.value!, { return false;
'name': templateName.value, }
'template': { try {
'data': groups.toJson(), await LoadingUtil.to.show();
'totalCost': totalCost.value, if (templateId.value == null || isSaveAs) {
'totalPrice': totalPrice.value, await Api.createSaleQuotationTemplate({
'formula': formula.value '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();
} }
await getTemplates();
await SnackBarUtil().success('已保存');
return true; return true;
} }
@ -303,6 +340,7 @@ class SaleQuotationController extends GetxController {
ModalUtil.showGeneralDialog( ModalUtil.showGeneralDialog(
content: SkMutilSearchMore<SaleQuotationModel>( content: SkMutilSearchMore<SaleQuotationModel>(
controller: controller, controller: controller,
title: '请选择分组',
enablePullUp: false, enablePullUp: false,
enablePullDown: true, enablePullDown: true,
onOk: (List<int> indexes) { onOk: (List<int> indexes) {
@ -346,21 +384,30 @@ class SaleQuotationController extends GetxController {
content: SkMutilSearchMore<SaleQuotationItemModel>( content: SkMutilSearchMore<SaleQuotationItemModel>(
controller: controller, controller: controller,
enablePullUp: false, enablePullUp: false,
title: '请选择配件产品',
enablePullDown: true, enablePullDown: true,
isDialog: true, isDialog: true,
onOk: (List<int> indexes) { onOk: (List<int> indexes) {
groups[groupIndex] groups[groupIndex]
.items .items
.addAll(controller.list.where((element) { .addAll(controller.list.where((element) {
return indexes.contains(controller.list.indexOf(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(); saveToLocal();
}, },
leadingBuilder: (index) { leadingBuilder: (index) {
return Container( return Container(
padding: EdgeInsets.symmetric( padding: EdgeInsets.symmetric(
horizontal: ScreenAdaper.width(5), horizontal: ScreenAdaper.width(5),
vertical: ScreenAdaper.height(10)), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -369,28 +416,41 @@ class SaleQuotationController extends GetxController {
Text( Text(
controller.list[index].name, controller.list[index].name,
style: TextStyle( style: TextStyle(
fontSize: ScreenAdaper.height(30)), fontSize: ScreenAdaper.height(30),
fontWeight: FontWeight.w600),
), ),
], ],
), ),
if (controller.list[index].spec != null) if (controller.list[index].unit != null)
Row( Row(
children: [ children: [
Text( Text(
'型号${controller.list[index].spec ?? ''}', '单位${controller.list[index].unit ?? ''}',
textAlign: TextAlign.start, textAlign: TextAlign.start,
style: TextStyle( style: TextStyle(
fontSize: ScreenAdaper.height(25)), 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( Row(
children: [ children: [
Text( Text(
'单价:¥${controller.list[index].cost}', '单价:¥${controller.list[index].unitPrice}',
textAlign: TextAlign.start, textAlign: TextAlign.start,
style: TextStyle( style: TextStyle(
fontSize: ScreenAdaper.height(25)), fontSize: ScreenAdaper.height(23)),
), ),
], ],
), ),
@ -401,7 +461,7 @@ class SaleQuotationController extends GetxController {
'备注:${controller.list[index].remark ?? ''}', '备注:${controller.list[index].remark ?? ''}',
textAlign: TextAlign.start, textAlign: TextAlign.start,
style: TextStyle( style: TextStyle(
fontSize: ScreenAdaper.height(25)), fontSize: ScreenAdaper.height(23)),
), ),
], ],
), ),
@ -427,6 +487,82 @@ class SaleQuotationController extends GetxController {
templateId.value = null; templateId.value = null;
saveToLocal(); 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 class GroupSearchMoreController
@ -472,30 +608,31 @@ class ItemSearchMoreController
List<SaleQuotationItemModel> newList = componets List<SaleQuotationItemModel> newList = componets
.map((e) => SaleQuotationItemModel( .map((e) => SaleQuotationItemModel(
name: e.name!, name: e.name!,
cost: e.unitPrice ?? 0.0, unitPrice: e.unitPrice ?? 0.0,
unit: e.unit?.label ?? '', unit: e.unit?.label ?? '',
componentSpecification: e.componentSpecification,
remark: e.remark)) remark: e.remark))
.toList(); .toList();
// List<SaleQuotationItemModel> newList = [ // List<SaleQuotationItemModel> newList = [
// SaleQuotationItemModel( // SaleQuotationItemModel(
// name: '矿用本安型支架控制器', unit: '', spec: 'ZDYZ-Z', cost: 4700), // name: '矿用本安型支架控制器', unit: '', componentSpecification: 'ZDYZ-Z', unitPrice: 4700),
// SaleQuotationItemModel(name: '矿用本安型电磁阀驱动器', cost: 1200), // SaleQuotationItemModel(name: '矿用本安型电磁阀驱动器', unitPrice: 1200),
// SaleQuotationItemModel(name: '矿用隔爆兼本安型电源', cost: 5700), // SaleQuotationItemModel(name: '矿用隔爆兼本安型电源', unitPrice: 5700),
// SaleQuotationItemModel(name: '矿用本安型隔离耦合器', cost: 1200), // SaleQuotationItemModel(name: '矿用本安型隔离耦合器', unitPrice: 1200),
// SaleQuotationItemModel(name: '钢丝编织橡胶护套连接器', cost: 600), // SaleQuotationItemModel(name: '钢丝编织橡胶护套连接器', unitPrice: 600),
// SaleQuotationItemModel(name: '钢丝编织橡胶护套连接器', remark: '控制器-控制器', cost: 400), // SaleQuotationItemModel(name: '钢丝编织橡胶护套连接器', remark: '控制器-控制器', unitPrice: 400),
// SaleQuotationItemModel(name: '钢丝编织橡胶护套连接器', remark: '控制器-驱动器', cost: 500), // SaleQuotationItemModel(name: '钢丝编织橡胶护套连接器', remark: '控制器-驱动器', unitPrice: 500),
// SaleQuotationItemModel( // SaleQuotationItemModel(
// name: '钢丝编织橡胶护套连接器', remark: '控制器-隔离耦合器', cost: 2000), // name: '钢丝编织橡胶护套连接器', remark: '控制器-隔离耦合器', unitPrice: 2000),
// SaleQuotationItemModel(name: '矿用本安型支架控制器', cost: 4700), // SaleQuotationItemModel(name: '矿用本安型支架控制器', unitPrice: 4700),
// SaleQuotationItemModel(name: '矿用本安型电磁阀驱动器', cost: 1200), // SaleQuotationItemModel(name: '矿用本安型电磁阀驱动器', unitPrice: 1200),
// SaleQuotationItemModel(name: '矿用隔爆兼本安型电源', cost: 5700), // SaleQuotationItemModel(name: '矿用隔爆兼本安型电源', unitPrice: 5700),
// SaleQuotationItemModel( // SaleQuotationItemModel(
// name: '电液换向阀(10功能10接口)', remark: '中间过渡架主阀组', cost: 13200), // name: '电液换向阀(10功能10接口)', remark: '中间过渡架主阀组', unitPrice: 13200),
// SaleQuotationItemModel( // SaleQuotationItemModel(
// name: '电液换向阀(20功能20接口)', remark: '端头架主阀组', cost: 26500), // name: '电液换向阀(20功能20接口)', remark: '端头架主阀组', unitPrice: 26500),
// SaleQuotationItemModel( // SaleQuotationItemModel(
// name: '自动反冲洗过滤装置', remark: '流量:900L/min,过滤精度25μm', cost: 2000), // name: '自动反冲洗过滤装置', remark: '流量:900L/min,过滤精度25μm', unitPrice: 2000),
// SaleQuotationItemModel(name: '全自动反冲洗过滤器电缆', remark: '控制器-自动反冲洗'), // SaleQuotationItemModel(name: '全自动反冲洗过滤器电缆', remark: '控制器-自动反冲洗'),
// SaleQuotationItemModel(name: '矿用本安型位移传感器'), // SaleQuotationItemModel(name: '矿用本安型位移传感器'),
// SaleQuotationItemModel(name: '矿用本安型压力传感器'), // SaleQuotationItemModel(name: '矿用本安型压力传感器'),

View File

@ -4,6 +4,7 @@ import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:sk_base_mobile/app_theme.dart'; import 'package:sk_base_mobile/app_theme.dart';
import 'package:sk_base_mobile/models/sale_quotation.model.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/components/sale_quotation_drawer.dart';
import 'package:sk_base_mobile/screens/sale_quotation/sale_quotation.controller.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/common.util.dart';
@ -37,6 +38,32 @@ class SaleQuotationPage extends StatelessWidget {
child: SaleQuotationEndDrawer(), child: SaleQuotationEndDrawer(),
), ),
appBar: SkAppbar( 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}', title: '报价计算-${controller.templateName}',
action: [ action: [
IconButton( IconButton(
@ -378,7 +405,7 @@ class SaleQuotationPage extends StatelessWidget {
Widget buildRow(int groupIndex, int rowIndex) { Widget buildRow(int groupIndex, int rowIndex) {
final subTextStyle = final subTextStyle =
TextStyle(color: AppTheme.grey, fontSize: ScreenAdaper.height(25)); TextStyle(color: AppTheme.grey, fontSize: ScreenAdaper.height(23));
return Slidable( return Slidable(
key: UniqueKey(), key: UniqueKey(),
endActionPane: ActionPane( endActionPane: ActionPane(
@ -429,16 +456,18 @@ class SaleQuotationPage extends StatelessWidget {
), ),
], ],
), ),
if (controller.groups[groupIndex].items[rowIndex].spec != if (controller.groups[groupIndex].items[rowIndex]
.componentSpecification !=
null) null)
Row( Row(
children: [ children: [
// Text(
// '规格、型号: ',
// style: subTextStyle,
// ),
Text( Text(
'规格、型号: ', controller.groups[groupIndex].items[rowIndex]
style: subTextStyle, .componentSpecification ??
),
Text(
controller.groups[groupIndex].items[rowIndex].spec ??
'', '',
style: subTextStyle, style: subTextStyle,
), ),
@ -509,16 +538,16 @@ class SaleQuotationPage extends StatelessWidget {
alignment: Alignment.center, alignment: Alignment.center,
width: ScreenAdaper.width(unitPriceWidth), width: ScreenAdaper.width(unitPriceWidth),
child: Text( child: Text(
CommonUtil.toNumberWithout0( CommonUtil.toNumberWithout0(controller
controller.groups[groupIndex].items[rowIndex].cost), .groups[groupIndex].items[rowIndex].unitPrice),
style: TextStyle(fontSize: ScreenAdaper.height(25)), style: TextStyle(fontSize: ScreenAdaper.height(25)),
), ),
), ),
groupIndex: groupIndex, groupIndex: groupIndex,
rowIndex: rowIndex, rowIndex: rowIndex,
field: 'cost', field: 'unitPrice',
inputWidth: ScreenAdaper.width(unitPriceWidth), inputWidth: ScreenAdaper.width(unitPriceWidth),
value: controller.groups[groupIndex].items[rowIndex].cost), value: controller.groups[groupIndex].items[rowIndex].unitPrice),
buildEditCell<double>( buildEditCell<double>(
Container( Container(
alignment: Alignment.center, alignment: Alignment.center,

View File

@ -6,6 +6,7 @@ import 'package:sk_base_mobile/util/screen_adaper_util.dart';
class LoadingUtil extends GetxService { class LoadingUtil extends GetxService {
static LoadingUtil get to => Get.find(); static LoadingUtil get to => Get.find();
// must use LoadingUtil.to.show() instead of LoadingUtil().show()
OverlayEntry? _loadingOverlay; OverlayEntry? _loadingOverlay;
Future<LoadingUtil> init() async { Future<LoadingUtil> init() async {
@ -17,17 +18,17 @@ class LoadingUtil extends GetxService {
} }
Future<void> dismiss() async { Future<void> dismiss() async {
return hideLoading(); return _hideLoading();
} }
showLoading({String? status}) async { showLoading({String? status}) async {
hideLoading(); _hideLoading();
_loadingOverlay = OverlayEntry( _loadingOverlay = OverlayEntry(
builder: (BuildContext context) { builder: (BuildContext context) {
return PopScope( return PopScope(
onPopInvoked: (_) { onPopInvoked: (_) {
hideLoading(); _hideLoading();
}, },
child: Stack( child: Stack(
children: <Widget>[ children: <Widget>[
@ -60,7 +61,7 @@ class LoadingUtil extends GetxService {
} }
} }
hideLoading() { _hideLoading() {
_loadingOverlay?.remove(); _loadingOverlay?.remove();
_loadingOverlay = null; _loadingOverlay = null;
} }

View File

@ -10,8 +10,10 @@ class ModalUtil {
{String? title, {String? title,
String? contentText, String? contentText,
String? confirmText, String? confirmText,
String? cancelText,
Widget? content, Widget? content,
VoidCallback? onConfirm, VoidCallback? onConfirm,
VoidCallback? onCancel,
Widget Function(BuildContext)? builder, Widget Function(BuildContext)? builder,
EdgeInsetsGeometry? contentPadding, EdgeInsetsGeometry? contentPadding,
bool showActions = true, bool showActions = true,
@ -98,13 +100,14 @@ class ModalUtil {
onTap: () { onTap: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
confirmed = false; confirmed = false;
if (onCancel != null) onCancel();
}, },
child: Container( child: Container(
padding: EdgeInsets.symmetric( padding: EdgeInsets.symmetric(
vertical: ScreenAdaper.height(15)), vertical: ScreenAdaper.height(15)),
alignment: Alignment.center, alignment: Alignment.center,
child: Text( child: Text(
'取消', cancelText ?? '取消',
style: TextStyle( style: TextStyle(
color: Colors.black, color: Colors.black,
fontSize: ScreenAdaper.height(30)), fontSize: ScreenAdaper.height(30)),
@ -113,9 +116,9 @@ class ModalUtil {
Expanded( Expanded(
child: InkWell( child: InkWell(
onTap: () { onTap: () {
if (onConfirm != null) onConfirm();
Navigator.pop(context); Navigator.pop(context);
confirmed = true; confirmed = true;
if (onConfirm != null) onConfirm();
}, },
child: Container( child: Container(
padding: EdgeInsets.symmetric( padding: EdgeInsets.symmetric(

View File

@ -8,6 +8,7 @@ class SkAppbar extends StatelessWidget implements PreferredSizeWidget {
final PreferredSizeWidget? bottom; final PreferredSizeWidget? bottom;
final Color? backgroundColor; final Color? backgroundColor;
final Color? iconAndTextColor; final Color? iconAndTextColor;
final Function()? onPop;
const SkAppbar( const SkAppbar(
{super.key, {super.key,
required this.title, required this.title,
@ -15,6 +16,7 @@ class SkAppbar extends StatelessWidget implements PreferredSizeWidget {
this.hideLeading = false, this.hideLeading = false,
this.backgroundColor, this.backgroundColor,
this.iconAndTextColor, this.iconAndTextColor,
this.onPop,
this.bottom}); this.bottom});
@override @override
@ -34,8 +36,12 @@ class SkAppbar extends StatelessWidget implements PreferredSizeWidget {
size: ScreenAdaper.height(40), size: ScreenAdaper.height(40),
color: iconAndTextColor, color: iconAndTextColor,
), ),
onPressed: () { onPressed: () async {
Navigator.pop(context); if (onPop != null) {
onPop!();
} else {
Navigator.pop(context);
}
}, },
), ),
title: Text( title: Text(

View File

@ -11,11 +11,12 @@ class SkAvatar extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ClipRRect( return ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(40)), borderRadius: BorderRadius.circular(ScreenAdaper.sp(80)),
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey[200],
border: Border.all(color: AppTheme.dividerColor), border: Border.all(color: AppTheme.dividerColor),
borderRadius: const BorderRadius.all(Radius.circular(40))), borderRadius: BorderRadius.circular(ScreenAdaper.sp(80))),
child: FadeInCacheImage( child: FadeInCacheImage(
defaultWidget: Icon(Icons.person_2_outlined, defaultWidget: Icon(Icons.person_2_outlined,
size: ScreenAdaper.height(60), color: AppTheme.grey), size: ScreenAdaper.height(60), color: AppTheme.grey),

View File

@ -0,0 +1,20 @@
import 'package:flutter/material.dart';
import 'package:sk_base_mobile/config.dart';
import 'package:sk_base_mobile/util/device.util.dart';
import 'package:sk_base_mobile/util/screen_adaper_util.dart';
import 'package:sk_base_mobile/widgets/core/sk_ink.dart';
class CustomContactIcon extends StatelessWidget {
const CustomContactIcon({super.key});
@override
Widget build(BuildContext context) {
return SkInk(
onTap: () {
DeviceUtil.callPhone(GloablConfig.SUPPORT_PHONE);
},
child: Image(
height: ScreenAdaper.height(70),
image: AssetImage('assets/icons/help_desk.png')));
}
}

View File

@ -11,24 +11,27 @@ class Empty extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return GestureDetector(
onTap: onTap, onTap: onTap,
child: Center( child: Container(
child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [ padding: EdgeInsets.symmetric(horizontal: ScreenAdaper.height(10)),
Center( child: Row(
child: icon ?? mainAxisSize: MainAxisSize.min,
Icon(Icons.error_outline, mainAxisAlignment: MainAxisAlignment.center,
size: ScreenAdaper.height(40), children: [
color: AppTheme.dangerColor)), Center(
SizedBox( child: icon ??
width: ScreenAdaper.width(10), Icon(Icons.error_outline,
), size: ScreenAdaper.height(40),
Text( color: AppTheme.dangerColor)),
text ?? '', SizedBox(
textAlign: TextAlign.center, width: ScreenAdaper.width(10),
style: TextStyle(fontSize: ScreenAdaper.height(35)), ),
), Text(
]), text ?? '',
), textAlign: TextAlign.center,
); style: TextStyle(fontSize: ScreenAdaper.height(30)),
)
]),
));
} }
} }

View File

@ -21,6 +21,7 @@ class SkMutilSearchMore<T extends BaseSearchMoreModel> extends StatelessWidget {
final bool enablePullDown; final bool enablePullDown;
final bool isDialog; final bool isDialog;
final Function(int)? leadingBuilder; final Function(int)? leadingBuilder;
final String? title;
SkMutilSearchMore( SkMutilSearchMore(
{super.key, {super.key,
required this.controller, required this.controller,
@ -28,6 +29,7 @@ class SkMutilSearchMore<T extends BaseSearchMoreModel> extends StatelessWidget {
this.isDialog = true, this.isDialog = true,
this.beforeSelectedCheck, this.beforeSelectedCheck,
this.leadingBuilder, this.leadingBuilder,
this.title,
this.enablePullDown = true, this.enablePullDown = true,
this.enablePullUp = true}); this.enablePullUp = true});
final listTitleTextStyle = final listTitleTextStyle =
@ -78,7 +80,7 @@ class SkMutilSearchMore<T extends BaseSearchMoreModel> extends StatelessWidget {
children: [ children: [
Center( Center(
child: Text( child: Text(
'请选择', title ?? '请选择',
style: TextStyle( style: TextStyle(
fontSize: ScreenAdaper.height(30), fontSize: ScreenAdaper.height(30),
fontWeight: FontWeight.w600), fontWeight: FontWeight.w600),