feat: 电解控实时计算功能完成
This commit is contained in:
parent
49ddf62751
commit
10145b3af7
|
@ -2,11 +2,11 @@
|
|||
|
||||
// Global config
|
||||
class GloablConfig {
|
||||
// static const BASE_URL = "http://10.0.2.2:8001/api/";
|
||||
// static const OSS_URL = "http://10.0.2.2:8001";
|
||||
static const BASE_URL = "http://10.0.2.2:8001/api/";
|
||||
static const OSS_URL = "http://10.0.2.2:8001";
|
||||
|
||||
static const BASE_URL = "http://144.123.43.138:3001/api/";
|
||||
static const OSS_URL = "http://144.123.43.138:3001";
|
||||
// static const BASE_URL = "http://144.123.43.138:3001/api/";
|
||||
// static const OSS_URL = "http://144.123.43.138:3001";
|
||||
// static const BASE_URL = "http://192.168.60.220:8001/api/";
|
||||
// static const OSS_URL = "http://192.168.60.220:8001";
|
||||
static const DOMAIN_NAME = "山矿通";
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
import 'package:get/get_rx/src/rx_types/rx_types.dart';
|
||||
|
||||
class BaseSearchMoreModel {
|
||||
RxBool isExpanded = true.obs;
|
||||
String? name;
|
||||
}
|
|
@ -1,8 +1,9 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get_rx/src/rx_types/rx_types.dart';
|
||||
import 'package:pull_to_refresh/pull_to_refresh.dart';
|
||||
import 'package:sk_base_mobile/models/base_search_more.model.dart';
|
||||
|
||||
mixin BaseSearchMore<T> {
|
||||
mixin BaseSearchMoreController<T extends BaseSearchMoreModel> {
|
||||
RxList<T> list = RxList([]);
|
||||
RxString searchKey = ''.obs;
|
||||
final searchBarTextConroller = TextEditingController();
|
||||
|
@ -10,6 +11,7 @@ mixin BaseSearchMore<T> {
|
|||
int page = 1;
|
||||
int limit = 15;
|
||||
int total = 0;
|
||||
RxList<int> selectedIndex = RxList([]);
|
||||
getData({bool isRefresh = false}) {}
|
||||
|
||||
Future<void> onRefresh() async {
|
|
@ -1,22 +1,35 @@
|
|||
import 'package:get/get_rx/src/rx_types/rx_types.dart';
|
||||
import 'package:sk_base_mobile/models/base_search_more.model.dart';
|
||||
|
||||
class SaleQuotationModel {
|
||||
class SaleQuotationModel extends BaseSearchMoreModel {
|
||||
final String name;
|
||||
RxBool isExpanded = true.obs;
|
||||
RxList<SaleQuotationItemModel> items;
|
||||
SaleQuotationModel({required this.name, required this.items});
|
||||
// 生成fromjson
|
||||
SaleQuotationModel.fromJson(Map<String, dynamic> json)
|
||||
: name = json['name'],
|
||||
items = (json['items'] as List)
|
||||
.map((e) => SaleQuotationItemModel.fromJson(e))
|
||||
.toList()
|
||||
.obs;
|
||||
// 生成tojson
|
||||
Map<String, dynamic> toJson() => {
|
||||
'name': name,
|
||||
'items': items.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
|
||||
class SaleQuotationItemModel {
|
||||
class SaleQuotationItemModel extends BaseSearchMoreModel {
|
||||
final String name;
|
||||
// 规格
|
||||
final String? spec;
|
||||
final String? unit;
|
||||
final String? remark;
|
||||
// 成本
|
||||
final double cost;
|
||||
final int quantity;
|
||||
final double amount;
|
||||
int cost;
|
||||
int quantity;
|
||||
int amount;
|
||||
|
||||
SaleQuotationItemModel(
|
||||
{required this.name,
|
||||
this.spec,
|
||||
|
@ -25,4 +38,22 @@ class SaleQuotationItemModel {
|
|||
this.cost = 0,
|
||||
this.quantity = 0,
|
||||
this.amount = 0});
|
||||
SaleQuotationItemModel.fromJson(Map<String, dynamic> json)
|
||||
: name = json['name'],
|
||||
spec = json['spec'],
|
||||
unit = json['unit'],
|
||||
remark = json['remark'],
|
||||
cost = json['cost'],
|
||||
quantity = json['quantity'],
|
||||
amount = json['amount'];
|
||||
// 生成tojson
|
||||
Map<String, dynamic> toJson() => {
|
||||
'name': name,
|
||||
'spec': spec,
|
||||
'unit': unit,
|
||||
'remark': remark,
|
||||
'cost': cost,
|
||||
'quantity': quantity,
|
||||
'amount': amount,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -172,8 +172,8 @@ class LoginScreen extends StatelessWidget {
|
|||
|
||||
Widget buildUserNameInput() {
|
||||
return TextFormField(
|
||||
// 英文数字键盘
|
||||
obscureText: true,
|
||||
// 纯英文数字键盘
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: Icon(
|
||||
Icons.person_2_outlined,
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
import 'package:flutter/material.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/gradient_button.dart';
|
||||
import 'package:sk_base_mobile/widgets/sk_appbar.dart';
|
||||
|
||||
class SaleQuotationEndDrawer extends StatelessWidget {
|
||||
const SaleQuotationEndDrawer({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: SkAppbar(title: '工具栏', hideLeading: true),
|
||||
body: buildBody(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildBody() {
|
||||
return Column(
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text('选择报价模板'),
|
||||
onTap: () {},
|
||||
),
|
||||
Divider(
|
||||
color: AppTheme.dividerColor,
|
||||
height: 1,
|
||||
),
|
||||
Spacer(),
|
||||
GradientButton(
|
||||
icon: Icon(
|
||||
Icons.save,
|
||||
color: AppTheme.nearlyWhite,
|
||||
size: ScreenAdaper.height(40),
|
||||
),
|
||||
buttonText: '保存为模板',
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -3,15 +3,12 @@ import 'dart:async';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pull_to_refresh/pull_to_refresh.dart';
|
||||
import 'package:sk_base_mobile/apis/index.dart';
|
||||
import 'package:sk_base_mobile/app_theme.dart';
|
||||
import 'package:sk_base_mobile/constants/bg_color.dart';
|
||||
import 'package:sk_base_mobile/models/sale_quotation.model.dart';
|
||||
import 'package:sk_base_mobile/models/user_info.model.dart';
|
||||
import 'package:sk_base_mobile/util/debouncer.dart';
|
||||
import 'package:sk_base_mobile/util/screen_adaper_util.dart';
|
||||
import 'package:sk_base_mobile/widgets/empty.dart';
|
||||
import 'package:sk_base_mobile/widgets/fade_in_cache_image.dart';
|
||||
|
||||
class SaleQuotationGroupSearch extends StatelessWidget {
|
||||
final Function(SaleQuotationModel)? onSelected;
|
||||
|
|
|
@ -1,8 +1,15 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:decimal/decimal.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:sk_base_mobile/models/base_search_more.dart';
|
||||
import 'package:math_expressions/math_expressions.dart';
|
||||
import 'package:sk_base_mobile/models/base_search_more_controller.dart';
|
||||
import 'package:sk_base_mobile/models/sale_quotation.model.dart';
|
||||
import 'package:sk_base_mobile/screens/sale_quotation/components/sale_quotation_group_search.dart';
|
||||
import 'package:sk_base_mobile/services/storage.service.dart';
|
||||
import 'package:sk_base_mobile/util/snack_bar.util.dart';
|
||||
import 'package:sk_base_mobile/widgets/core/sk_muti_search_more.dart';
|
||||
import 'package:sk_base_mobile/widgets/core/sk_single_search_more.dart';
|
||||
import 'package:sk_base_mobile/util/modal.util.dart';
|
||||
import 'package:sk_base_mobile/util/screen_adaper_util.dart';
|
||||
|
@ -11,6 +18,7 @@ import 'package:pinyin/pinyin.dart';
|
|||
class SaleQuotationController extends GetxController {
|
||||
static SaleQuotationController get to => Get.find();
|
||||
final RxList editingcell = RxList([null, null, null]);
|
||||
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
RxList<SaleQuotationItemModel> products = RxList([
|
||||
SaleQuotationItemModel(name: '矿用本安型支架控制器', unit: '台', spec: 'ZDYZ-Z'),
|
||||
SaleQuotationItemModel(name: '矿用本安型电磁阀驱动器'),
|
||||
|
@ -34,90 +42,178 @@ class SaleQuotationController extends GetxController {
|
|||
SaleQuotationItemModel(name: '各类安装附件'),
|
||||
]);
|
||||
RxList<SaleQuotationModel> groups = RxList<SaleQuotationModel>([]);
|
||||
|
||||
RxInt totalCost = RxInt(0);
|
||||
RxDouble totalPrice = RxDouble(0.0);
|
||||
RxBool isFormulaEditing = false.obs;
|
||||
RxString formula = '成本 * 1.3 / 0.864'.obs;
|
||||
@override
|
||||
void onReady() {
|
||||
init();
|
||||
super.onReady();
|
||||
}
|
||||
|
||||
Future<void> addGroup() async {
|
||||
Future<void> init() async {
|
||||
String? salesQuotation = StorageService.to.getString('salesQuotation');
|
||||
if (salesQuotation != null) {
|
||||
groups.assignAll((jsonDecode(salesQuotation) as List)
|
||||
.map((e) => SaleQuotationModel.fromJson(e))
|
||||
.toList());
|
||||
calculateTotal();
|
||||
}
|
||||
}
|
||||
|
||||
void calculateTotal({String? newFormula}) {
|
||||
//计算groups中所有items中的amout总和
|
||||
totalCost.value = groups.fold<int>(0, (previousValue, element) {
|
||||
return previousValue +
|
||||
element.items.fold<int>(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 cost = Decimal.fromInt(0);
|
||||
Decimal amount = Decimal.fromInt(0);
|
||||
if (data.quantity != 0) {
|
||||
quantity = Decimal.parse('${data.quantity}');
|
||||
}
|
||||
if (data.cost != 0) {
|
||||
cost = Decimal.parse('${data.cost}');
|
||||
}
|
||||
if (data.amount != 0) {
|
||||
amount = Decimal.parse('${data.amount}');
|
||||
}
|
||||
// 入库一般是先输入单价和数量,然后计算单价
|
||||
if (changedField != 'amount') {
|
||||
Decimal result = cost * quantity;
|
||||
data.amount = result != Decimal.zero ? result.toBigInt().toInt() : 0;
|
||||
}
|
||||
|
||||
if (changedField == 'amount' && quantity != Decimal.zero) {
|
||||
Decimal result =
|
||||
(amount / quantity).toDecimal(scaleOnInfinitePrecision: 10);
|
||||
data.cost = result != Decimal.zero ? result.toBigInt().toInt() : 0;
|
||||
} else if (changedField != 'amount') {
|
||||
Decimal result = (cost * quantity);
|
||||
data.amount = result != Decimal.zero ? result.toBigInt().toInt() : 0;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
void saveChanges(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]);
|
||||
save();
|
||||
}
|
||||
|
||||
Future<void> save() async {
|
||||
await StorageService.to
|
||||
.setString('salesQuotation', jsonEncode(groups.toJson()));
|
||||
// SnackBarUtil().success('已保存到本地');
|
||||
}
|
||||
|
||||
void addGroup() async {
|
||||
final controller = Get.put(GroupSearchMoreController());
|
||||
// 选择组件 选择分组
|
||||
ModalUtil.showGeneralDialog(
|
||||
content: SkSingleSearchMore<SaleQuotationModel>(
|
||||
content: SkMutilSearchMore<SaleQuotationModel>(
|
||||
controller: controller,
|
||||
enablePullUp: false,
|
||||
enablePullDown: true,
|
||||
itemBuilder: (_, index) {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
Get.back();
|
||||
groups.add(controller.list[index]);
|
||||
},
|
||||
child: 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(25)),
|
||||
),
|
||||
],
|
||||
onOk: (List<int> indexes) {
|
||||
groups.addAll(controller.list.where((element) {
|
||||
return indexes.contains(controller.list.indexOf(element));
|
||||
}));
|
||||
save();
|
||||
},
|
||||
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<GroupSearchController>());
|
||||
calculateTotal();
|
||||
}
|
||||
|
||||
Future<void> removeGroup(int index) async {
|
||||
void removeGroup(int index) {
|
||||
groups.removeAt(index);
|
||||
calculateTotal();
|
||||
save();
|
||||
}
|
||||
|
||||
Future<void> addItems(int groupIndex) async {
|
||||
void addItems(int groupIndex) async {
|
||||
final controller = Get.put(ItemSearchMoreController());
|
||||
// 选择产品
|
||||
ModalUtil.showGeneralDialog(
|
||||
content: SkSingleSearchMore<SaleQuotationItemModel>(
|
||||
content: SkMutilSearchMore<SaleQuotationItemModel>(
|
||||
controller: controller,
|
||||
enablePullUp: false,
|
||||
enablePullDown: true,
|
||||
isDialog: true,
|
||||
itemBuilder: (_, index) {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
Get.back();
|
||||
groups[groupIndex].items.add(controller.list[index]);
|
||||
},
|
||||
child: 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(25)),
|
||||
),
|
||||
],
|
||||
),
|
||||
));
|
||||
onOk: (List<int> indexes) {
|
||||
groups[groupIndex].items.addAll(products.where((element) {
|
||||
return indexes.contains(products.indexOf(element));
|
||||
}));
|
||||
save();
|
||||
},
|
||||
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(25)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
// onSelected: (SaleQuotationItemModel info) {
|
||||
// Get.back();
|
||||
// // groups.add(info);
|
||||
// },
|
||||
),
|
||||
width: Get.width - ScreenAdaper.width(50))
|
||||
.then((value) => Get.delete<ItemSearchMoreController>());
|
||||
calculateTotal();
|
||||
}
|
||||
|
||||
void removeItem(int groupIndex, int rowIndex) {
|
||||
groups[groupIndex].items.removeAt(rowIndex);
|
||||
calculateTotal();
|
||||
}
|
||||
}
|
||||
|
||||
class GroupSearchMoreController extends GetxController
|
||||
with BaseSearchMore<SaleQuotationModel> {
|
||||
with BaseSearchMoreController<SaleQuotationModel> {
|
||||
@override
|
||||
Future<List<SaleQuotationModel>> getData({bool isRefresh = false}) async {
|
||||
List<SaleQuotationModel> newList = [
|
||||
|
@ -139,7 +235,7 @@ class GroupSearchMoreController extends GetxController
|
|||
}
|
||||
|
||||
class ItemSearchMoreController extends GetxController
|
||||
with BaseSearchMore<SaleQuotationItemModel> {
|
||||
with BaseSearchMoreController<SaleQuotationItemModel> {
|
||||
@override
|
||||
Future<List<SaleQuotationItemModel>> getData({bool isRefresh = false}) async {
|
||||
List<SaleQuotationItemModel> newList = [
|
||||
|
|
|
@ -1,82 +1,228 @@
|
|||
import 'dart:ffi';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:date_format/date_format.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/sk_appbar.dart';
|
||||
import 'package:flutter_sticky_header/flutter_sticky_header.dart';
|
||||
import 'package:math_expressions/math_expressions.dart';
|
||||
import 'dart:math' as math;
|
||||
|
||||
class SaleQuotationPage extends StatelessWidget {
|
||||
SaleQuotationPage({super.key});
|
||||
final controller = Get.put(SaleQuotationController());
|
||||
final quantityWidth = 55.0;
|
||||
final unitPriceWidth = 80.0;
|
||||
final amountWidth = 85.0;
|
||||
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 = Color.fromARGB(255, 238, 238, 238);
|
||||
final headerBgcolor = const Color.fromARGB(255, 238, 238, 238);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () {
|
||||
controller.addGroup();
|
||||
},
|
||||
child: const Icon(Icons.add),
|
||||
key: controller.scaffoldKey,
|
||||
endDrawer: Drawer(
|
||||
// 从右到左出现
|
||||
child: SaleQuotationEndDrawer(),
|
||||
),
|
||||
appBar: SkAppbar(
|
||||
title: '报价计算',
|
||||
action: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
controller.addGroup();
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.save, color: AppTheme.white),
|
||||
SizedBox(
|
||||
width: ScreenAdaper.width(10),
|
||||
),
|
||||
Text(
|
||||
'保存',
|
||||
style: TextStyle(
|
||||
color: AppTheme.white,
|
||||
fontSize: ScreenAdaper.height(25),
|
||||
fontWeight: FontWeight.w600),
|
||||
)
|
||||
],
|
||||
))
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
controller.scaffoldKey.currentState?.openEndDrawer();
|
||||
},
|
||||
icon: const Icon(Icons.more_horiz_outlined, color: AppTheme.white),
|
||||
)
|
||||
],
|
||||
),
|
||||
body: SafeArea(
|
||||
bottom: ScreenAdaper.isLandspace() ? false : true,
|
||||
child: Column(
|
||||
children: [
|
||||
builderHeader(),
|
||||
Expanded(
|
||||
child: Obx(() => controller.groups.isEmpty
|
||||
? Empty(text: '请先添加分组')
|
||||
: CustomScrollView(
|
||||
slivers: controller.groups
|
||||
.mapIndexed<Widget>(
|
||||
(index, e) => buildBody(e, index))
|
||||
.toList(),
|
||||
))),
|
||||
],
|
||||
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(10),
|
||||
// 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),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -100,7 +246,7 @@ class SaleQuotationPage extends StatelessWidget {
|
|||
const Spacer(),
|
||||
Container(
|
||||
alignment: Alignment.center,
|
||||
width: quantityWidth,
|
||||
width: ScreenAdaper.width(quantityWidth),
|
||||
// constraints: BoxConstraints(minWidth: quantityWidth),
|
||||
child: Text(
|
||||
'数量',
|
||||
|
@ -109,8 +255,8 @@ class SaleQuotationPage extends StatelessWidget {
|
|||
),
|
||||
Container(
|
||||
alignment: Alignment.center,
|
||||
width: unitPriceWidth,
|
||||
// constraints: BoxConstraints(minWidth: unitPriceWidth),
|
||||
width: ScreenAdaper.width(unitPriceWidth),
|
||||
// constraints: BoxConstraints(minWidth: ScreenAdaper.width(unitPriceWidth)),
|
||||
child: Text(
|
||||
'单价',
|
||||
style: headerTitleStyle,
|
||||
|
@ -118,7 +264,7 @@ class SaleQuotationPage extends StatelessWidget {
|
|||
),
|
||||
Container(
|
||||
alignment: Alignment.center,
|
||||
width: amountWidth,
|
||||
width: ScreenAdaper.width(amountWidth),
|
||||
child: Text(
|
||||
'总价',
|
||||
style: headerTitleStyle,
|
||||
|
@ -128,259 +274,291 @@ class SaleQuotationPage extends StatelessWidget {
|
|||
));
|
||||
}
|
||||
|
||||
Widget buildBody(SaleQuotationModel group, int groupIndex) {
|
||||
final titleStyle = TextStyle(
|
||||
fontSize: ScreenAdaper.height(30),
|
||||
color: AppTheme.nearlyBlack,
|
||||
fontWeight: FontWeight.w600);
|
||||
final subTextStyle =
|
||||
TextStyle(color: AppTheme.grey, fontSize: ScreenAdaper.height(30));
|
||||
|
||||
Widget buildBody(int groupIndex) {
|
||||
return SliverStickyHeader.builder(
|
||||
builder: (context, state) => 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(
|
||||
group.name,
|
||||
style: titleStyle,
|
||||
),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
ModalUtil.confirm(
|
||||
title: '确定要删除此组吗?',
|
||||
onConfirm: () {
|
||||
controller.removeGroup(groupIndex);
|
||||
});
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.remove_circle_outline_outlined,
|
||||
color: AppTheme.nearlyBlack,
|
||||
)),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
controller.addItems(groupIndex);
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.add_circle_outline,
|
||||
color: AppTheme.nearlyBlack,
|
||||
)),
|
||||
],
|
||||
))),
|
||||
builder: (context, state) => buildGroupHeader(groupIndex, state),
|
||||
sliver: Obx(() => !controller.groups[groupIndex].isExpanded.value
|
||||
? SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, i) => SizedBox(),
|
||||
(context, i) => const SizedBox(),
|
||||
childCount: 0,
|
||||
))
|
||||
: SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, int i) => Slidable(
|
||||
key: UniqueKey(),
|
||||
endActionPane: ActionPane(
|
||||
extentRatio: 0.2,
|
||||
motion: const DrawerMotion(),
|
||||
children: [
|
||||
SlidableAction(
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: (_) {
|
||||
controller.groups[groupIndex].items.removeAt(i);
|
||||
},
|
||||
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(
|
||||
group.items[i].name,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: ScreenAdaper.height(30)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (group.items[i].spec != null)
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'规格、型号: ',
|
||||
style: subTextStyle,
|
||||
),
|
||||
Text(
|
||||
group.items[i].spec ?? '',
|
||||
style: subTextStyle,
|
||||
),
|
||||
],
|
||||
),
|
||||
if (group.items[i].unit != null)
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'单位: ',
|
||||
style: subTextStyle,
|
||||
),
|
||||
Text(
|
||||
group.items[i].unit ?? '',
|
||||
style: subTextStyle,
|
||||
),
|
||||
],
|
||||
),
|
||||
if (group.items[i].remark != null)
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'备注: ',
|
||||
style: subTextStyle,
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'${group.items[i].remark ?? ''}${group.items[i].remark ?? ''}${controller.products[i].remark ?? ''}',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: subTextStyle,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
VerticalDivider(),
|
||||
buildEditCell(
|
||||
Container(
|
||||
alignment: Alignment.center,
|
||||
width: quantityWidth,
|
||||
child: Text(
|
||||
'${controller.groups[groupIndex].items[i].quantity}',
|
||||
style: TextStyle(
|
||||
fontSize: ScreenAdaper.height(25)),
|
||||
),
|
||||
),
|
||||
groupIndex: groupIndex,
|
||||
rowIndex: i,
|
||||
field: 'quantity',
|
||||
inputWidth: quantityWidth,
|
||||
value: controller
|
||||
.groups[groupIndex].items[i].quantity),
|
||||
buildEditCell(
|
||||
Container(
|
||||
alignment: Alignment.center,
|
||||
width: unitPriceWidth,
|
||||
child: Text(
|
||||
'${controller.groups[groupIndex].items[i].cost}',
|
||||
style: TextStyle(
|
||||
fontSize: ScreenAdaper.height(25)),
|
||||
),
|
||||
),
|
||||
groupIndex: groupIndex,
|
||||
rowIndex: i,
|
||||
field: 'cost',
|
||||
inputWidth: unitPriceWidth,
|
||||
value: controller.groups[groupIndex].items[i].cost),
|
||||
buildEditCell(
|
||||
Container(
|
||||
alignment: Alignment.center,
|
||||
width: amountWidth,
|
||||
child: Text(
|
||||
'${controller.groups[groupIndex].items[i].amount}',
|
||||
style: TextStyle(
|
||||
fontSize: ScreenAdaper.height(25)),
|
||||
),
|
||||
),
|
||||
groupIndex: groupIndex,
|
||||
rowIndex: i,
|
||||
field: 'amount',
|
||||
inputWidth: amountWidth,
|
||||
value:
|
||||
controller.groups[groupIndex].items[i].amount),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
(context, int i) => buildRow(groupIndex, i),
|
||||
childCount: !controller.groups[groupIndex].isExpanded.value
|
||||
? 0
|
||||
: group.items.length,
|
||||
: controller.groups[groupIndex].items.length,
|
||||
),
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildEditCell(Widget content,
|
||||
{required int groupIndex,
|
||||
required int rowIndex,
|
||||
required String field,
|
||||
required double inputWidth,
|
||||
required dynamic value}) {
|
||||
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.confirm(
|
||||
title: '确定要删除此组吗?',
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
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
|
||||
? Container(
|
||||
? SizedBox(
|
||||
width: inputWidth,
|
||||
child: SkNumberInput(
|
||||
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}')),
|
||||
textController:
|
||||
TextEditingController(text: '${value == 0 ? '' : value}')),
|
||||
)
|
||||
: InkWell(
|
||||
onTap: () {
|
||||
|
|
|
@ -0,0 +1,191 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pull_to_refresh/pull_to_refresh.dart';
|
||||
import 'package:sk_base_mobile/app_theme.dart';
|
||||
import 'package:sk_base_mobile/constants/bg_color.dart';
|
||||
import 'package:sk_base_mobile/models/base_search_more.model.dart';
|
||||
import 'package:sk_base_mobile/models/base_search_more_controller.dart';
|
||||
import 'package:sk_base_mobile/util/debouncer.dart';
|
||||
import 'package:sk_base_mobile/util/screen_adaper_util.dart';
|
||||
import 'package:sk_base_mobile/widgets/empty.dart';
|
||||
import 'package:sk_base_mobile/widgets/gradient_button.dart';
|
||||
|
||||
class SkMutilSearchMore<T extends BaseSearchMoreModel> extends StatelessWidget {
|
||||
final Function(List<int>)? onOk;
|
||||
final Function(T)? beforeSelectedCheck;
|
||||
// ignore: prefer_typing_uninitialized_variables
|
||||
final BaseSearchMoreController controller;
|
||||
final bool enablePullUp;
|
||||
final bool enablePullDown;
|
||||
final bool isDialog;
|
||||
|
||||
final Function(int)? leadingBuilder;
|
||||
SkMutilSearchMore(
|
||||
{super.key,
|
||||
required this.controller,
|
||||
this.onOk,
|
||||
this.isDialog = true,
|
||||
this.beforeSelectedCheck,
|
||||
this.leadingBuilder,
|
||||
this.enablePullDown = true,
|
||||
this.enablePullUp = true});
|
||||
final listTitleTextStyle =
|
||||
TextStyle(fontSize: ScreenAdaper.height(20), fontWeight: FontWeight.w600);
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: ScreenAdaper.width(20),
|
||||
vertical: ScreenAdaper.height(20)),
|
||||
child: Column(
|
||||
children: [
|
||||
buildDialogHeader(),
|
||||
SizedBox(
|
||||
height: ScreenAdaper.height(defaultPadding) / 2,
|
||||
),
|
||||
buildSearchBar(),
|
||||
SizedBox(
|
||||
height: ScreenAdaper.height(defaultPadding) / 2,
|
||||
),
|
||||
Expanded(child: buildList()),
|
||||
SizedBox(
|
||||
height: ScreenAdaper.height(defaultPadding) / 2,
|
||||
),
|
||||
GradientButton(
|
||||
onPressed: () {
|
||||
if (onOk != null) {
|
||||
onOk!(controller.selectedIndex);
|
||||
}
|
||||
if (isDialog) {
|
||||
Get.back();
|
||||
}
|
||||
},
|
||||
buttonText: '确定',
|
||||
)
|
||||
],
|
||||
));
|
||||
}
|
||||
|
||||
Widget buildDialogHeader() {
|
||||
return Container(
|
||||
height: ScreenAdaper.height(60),
|
||||
child: Stack(
|
||||
children: [
|
||||
Center(
|
||||
child: Text(
|
||||
'请选择',
|
||||
style: TextStyle(
|
||||
fontSize: ScreenAdaper.height(30),
|
||||
fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: 0,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
Get.back();
|
||||
},
|
||||
child: Icon(
|
||||
Icons.close,
|
||||
size: ScreenAdaper.height(40),
|
||||
)))
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildSearchBar() {
|
||||
final doSearch = debouncer((String value) {
|
||||
controller.searchKey.value = value;
|
||||
controller.onRefresh();
|
||||
}, delayTime: 500);
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 5,
|
||||
child: TextField(
|
||||
controller: controller.searchBarTextConroller,
|
||||
onChanged: (value) => doSearch(value),
|
||||
decoration: InputDecoration(
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
vertical: ScreenAdaper.height(10),
|
||||
horizontal: ScreenAdaper.width(10)),
|
||||
hintText: '名称',
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
prefixIcon: const Icon(Icons.search),
|
||||
// 当searchBarController有值时不显示
|
||||
suffixIcon: Obx(() => controller.searchKey.value.isEmpty
|
||||
? const SizedBox()
|
||||
: IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
controller.searchKey.value = '';
|
||||
controller.searchBarTextConroller.clear();
|
||||
doSearch('');
|
||||
},
|
||||
)),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
))),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildList() {
|
||||
return Obx(() => SmartRefresher(
|
||||
enablePullDown: enablePullDown,
|
||||
enablePullUp: enablePullUp,
|
||||
controller: controller.refreshController,
|
||||
onLoading: controller.onLoading,
|
||||
onRefresh: controller.onRefresh,
|
||||
child: controller.refreshController.isLoading
|
||||
? const SizedBox()
|
||||
: controller.list.isEmpty
|
||||
? const Center(
|
||||
child: Empty(text: '暂无数据'),
|
||||
)
|
||||
: ListView.separated(
|
||||
separatorBuilder: (context, index) => const Divider(
|
||||
color: AppTheme.dividerColor,
|
||||
height: 1,
|
||||
),
|
||||
itemCount: controller.list.length,
|
||||
itemBuilder: (_, index) => buildItem(index))));
|
||||
}
|
||||
|
||||
Widget buildItem(int index) {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
controller.selectedIndex.contains(index)
|
||||
? controller.selectedIndex.remove(index)
|
||||
: controller.selectedIndex.add(index);
|
||||
},
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: ScreenAdaper.width(5),
|
||||
vertical: ScreenAdaper.height(20)),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: leadingBuilder != null
|
||||
? leadingBuilder!(index)
|
||||
: Text(
|
||||
'-',
|
||||
style: TextStyle(fontSize: ScreenAdaper.height(30)),
|
||||
),
|
||||
),
|
||||
Obx(() => controller.selectedIndex.contains(index)
|
||||
? Icon(
|
||||
Icons.check_circle,
|
||||
color: Colors.green,
|
||||
size: ScreenAdaper.height(40),
|
||||
)
|
||||
: const SizedBox())
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -2,36 +2,42 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter/services.dart';
|
||||
import 'package:sk_base_mobile/app_theme.dart';
|
||||
import 'package:sk_base_mobile/util/screen_adaper_util.dart';
|
||||
import 'package:sk_base_mobile/util/snack_bar.util.dart';
|
||||
|
||||
class SkNumberInput extends StatefulWidget {
|
||||
class SkNumberInput<T> extends StatefulWidget {
|
||||
final TextEditingController textController;
|
||||
final Function(FocusNode)? onTap;
|
||||
final Function(String)? onChanged;
|
||||
final Function(dynamic)? onTapOutside;
|
||||
final Function(dynamic)? validator;
|
||||
|
||||
final bool isDense;
|
||||
final EdgeInsetsGeometry? contentPadding;
|
||||
final bool autoFocus;
|
||||
final bool isRequired;
|
||||
final String labelText;
|
||||
final String? hint;
|
||||
|
||||
SkNumberInput({
|
||||
super.key,
|
||||
required this.textController,
|
||||
this.onTap,
|
||||
this.onChanged,
|
||||
this.hint,
|
||||
this.isRequired = false,
|
||||
this.labelText = '',
|
||||
this.autoFocus = false,
|
||||
this.contentPadding,
|
||||
this.isDense = false,
|
||||
});
|
||||
ValueChanged<int>? onFieldSubmitted;
|
||||
SkNumberInput(
|
||||
{super.key,
|
||||
required this.textController,
|
||||
this.onTap,
|
||||
this.onChanged,
|
||||
this.hint,
|
||||
this.onTapOutside,
|
||||
this.isRequired = false,
|
||||
this.labelText = '',
|
||||
this.onFieldSubmitted,
|
||||
this.autoFocus = false,
|
||||
this.contentPadding,
|
||||
this.isDense = false,
|
||||
this.validator});
|
||||
|
||||
@override
|
||||
State<SkNumberInput> createState() => _SkNumberInputState();
|
||||
State<SkNumberInput> createState() => _SkNumberInputState<T>();
|
||||
}
|
||||
|
||||
class _SkNumberInputState extends State<SkNumberInput> {
|
||||
class _SkNumberInputState<T> extends State<SkNumberInput> {
|
||||
late FocusNode focusNode = FocusNode();
|
||||
@override
|
||||
void initState() {
|
||||
|
@ -53,8 +59,38 @@ class _SkNumberInputState extends State<SkNumberInput> {
|
|||
}
|
||||
},
|
||||
controller: widget.textController,
|
||||
onFieldSubmitted: (event) {
|
||||
if (widget.onTapOutside != null) {
|
||||
dynamic value;
|
||||
if (T == double) {
|
||||
value = double.tryParse(widget.textController.text);
|
||||
|
||||
widget.onTapOutside!(value as T);
|
||||
} else {
|
||||
value = int.tryParse(widget.textController.text);
|
||||
}
|
||||
if (widget.validator != null && !widget.validator!(value)) {
|
||||
return;
|
||||
}
|
||||
widget.onTapOutside!(value as T);
|
||||
}
|
||||
},
|
||||
onTapOutside: (event) {
|
||||
FocusScope.of(context).unfocus();
|
||||
if (widget.onTapOutside != null) {
|
||||
dynamic value;
|
||||
if (T == double) {
|
||||
value = double.tryParse(widget.textController.text);
|
||||
|
||||
widget.onTapOutside!(value as T);
|
||||
} else {
|
||||
value = int.tryParse(widget.textController.text);
|
||||
}
|
||||
if (widget.validator != null && !widget.validator!(value)) {
|
||||
return;
|
||||
}
|
||||
widget.onTapOutside!(value as T);
|
||||
FocusScope.of(context).unfocus();
|
||||
}
|
||||
},
|
||||
onChanged: widget.onChanged ?? (_) {},
|
||||
textAlign: TextAlign.center,
|
||||
|
|
|
@ -3,7 +3,7 @@ import 'package:get/get.dart';
|
|||
import 'package:pull_to_refresh/pull_to_refresh.dart';
|
||||
import 'package:sk_base_mobile/app_theme.dart';
|
||||
import 'package:sk_base_mobile/constants/bg_color.dart';
|
||||
import 'package:sk_base_mobile/models/base_search_more.dart';
|
||||
import 'package:sk_base_mobile/models/base_search_more_controller.dart';
|
||||
import 'package:sk_base_mobile/util/debouncer.dart';
|
||||
import 'package:sk_base_mobile/util/screen_adaper_util.dart';
|
||||
import 'package:sk_base_mobile/widgets/empty.dart';
|
||||
|
@ -12,7 +12,7 @@ class SkSingleSearchMore<T> extends StatelessWidget {
|
|||
final Function(T)? onSelected;
|
||||
final Function(T)? beforeSelectedCheck;
|
||||
// ignore: prefer_typing_uninitialized_variables
|
||||
final BaseSearchMore controller;
|
||||
final BaseSearchMoreController controller;
|
||||
final bool enablePullUp;
|
||||
final bool enablePullDown;
|
||||
final bool isDialog;
|
||||
|
|
|
@ -10,20 +10,27 @@ class SkTextInput extends StatefulWidget {
|
|||
final String? hint;
|
||||
final bool isTextArea;
|
||||
final bool isDense;
|
||||
final Function(String)? onTapOutside;
|
||||
final Function(String)? onChanged;
|
||||
final String? Function(String?)? validator;
|
||||
final EdgeInsetsGeometry? contentPadding;
|
||||
final bool autoFocus;
|
||||
SkTextInput({
|
||||
super.key,
|
||||
required this.textController,
|
||||
this.onTap,
|
||||
this.hint,
|
||||
this.isRequired = false,
|
||||
this.labelText = '',
|
||||
this.isTextArea = false,
|
||||
this.autoFocus = false,
|
||||
this.contentPadding,
|
||||
this.isDense = false,
|
||||
});
|
||||
ValueChanged<String>? onFieldSubmitted;
|
||||
SkTextInput(
|
||||
{super.key,
|
||||
required this.textController,
|
||||
this.onTap,
|
||||
this.hint,
|
||||
this.onFieldSubmitted,
|
||||
this.isRequired = false,
|
||||
this.onTapOutside,
|
||||
this.labelText = '',
|
||||
this.onChanged,
|
||||
this.isTextArea = false,
|
||||
this.autoFocus = false,
|
||||
this.contentPadding,
|
||||
this.isDense = false,
|
||||
this.validator});
|
||||
|
||||
@override
|
||||
State<SkTextInput> createState() => _SkTextInputState();
|
||||
|
@ -31,6 +38,7 @@ class SkTextInput extends StatefulWidget {
|
|||
|
||||
class _SkTextInputState extends State<SkTextInput> {
|
||||
late FocusNode focusNode = FocusNode();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
@ -46,16 +54,28 @@ class _SkTextInputState extends State<SkTextInput> {
|
|||
return TextFormField(
|
||||
focusNode: focusNode,
|
||||
controller: widget.textController,
|
||||
onChanged: (String value) {
|
||||
if (widget.onChanged != null) {
|
||||
widget.onChanged!(value);
|
||||
}
|
||||
},
|
||||
onTapOutside: (event) {
|
||||
FocusScope.of(context).unfocus();
|
||||
if (widget.onTapOutside != null) {
|
||||
widget.onTapOutside!(widget.textController.text);
|
||||
FocusScope.of(context).unfocus();
|
||||
}
|
||||
},
|
||||
maxLines: widget.isTextArea ? 2 : 1, // 添加这行代码
|
||||
onTap: () {
|
||||
if (widget.onTap == null) {
|
||||
if (widget.onTap != null) {
|
||||
widget.onTap!(focusNode);
|
||||
}
|
||||
},
|
||||
onFieldSubmitted: widget.onFieldSubmitted,
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
validator: widget.validator,
|
||||
decoration: InputDecoration(
|
||||
errorStyle: const TextStyle(fontSize: 0, height: 0.01),
|
||||
contentPadding: widget.contentPadding,
|
||||
isDense: widget.isDense,
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
|
@ -75,8 +95,7 @@ class _SkTextInputState extends State<SkTextInput> {
|
|||
),
|
||||
]),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide:
|
||||
const BorderSide(color: AppTheme.primaryColorLight, width: 2),
|
||||
borderSide: BorderSide(color: AppTheme.primaryColorLight, width: 2),
|
||||
borderRadius: BorderRadius.circular(ScreenAdaper.sp(15))),
|
||||
hintText: widget.hint ?? '请输入',
|
||||
),
|
||||
|
|
|
@ -8,10 +8,12 @@ class GradientButton extends StatelessWidget {
|
|||
final VoidCallback? onPressed;
|
||||
final bool isLoading;
|
||||
final String buttonText;
|
||||
final Icon? icon;
|
||||
const GradientButton(
|
||||
{super.key,
|
||||
this.buttonText = TextEnum.createInventoryInOutBtnText,
|
||||
this.onPressed,
|
||||
this.icon,
|
||||
this.isLoading = false});
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -33,19 +35,30 @@ class GradientButton extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
onPressed: onPressed ?? () => {},
|
||||
child: isLoading
|
||||
? LoadingAnimationWidget.fourRotatingDots(
|
||||
color: AppTheme.nearlyWhite,
|
||||
size: ScreenAdaper.height(40),
|
||||
)
|
||||
: Text(
|
||||
buttonText,
|
||||
style: TextStyle(
|
||||
color: AppTheme.nearlyWhite,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: ScreenAdaper.height(25),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (icon != null) ...[
|
||||
icon!,
|
||||
SizedBox(
|
||||
width: ScreenAdaper.width(10),
|
||||
)
|
||||
],
|
||||
isLoading
|
||||
? LoadingAnimationWidget.fourRotatingDots(
|
||||
color: AppTheme.nearlyWhite,
|
||||
size: ScreenAdaper.height(40),
|
||||
)
|
||||
: Text(
|
||||
buttonText,
|
||||
style: TextStyle(
|
||||
color: AppTheme.nearlyWhite,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: ScreenAdaper.height(25),
|
||||
),
|
||||
)
|
||||
]),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -4,11 +4,24 @@ import 'package:sk_base_mobile/util/util.dart';
|
|||
class SkAppbar extends StatelessWidget implements PreferredSizeWidget {
|
||||
final String title;
|
||||
final List<Widget>? action;
|
||||
const SkAppbar({super.key, required this.title, this.action});
|
||||
final bool hideLeading;
|
||||
const SkAppbar(
|
||||
{super.key, required this.title, this.action, this.hideLeading = false});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppBar(
|
||||
leading: hideLeading
|
||||
? const SizedBox()
|
||||
: IconButton(
|
||||
icon: Icon(
|
||||
Icons.arrow_back_ios,
|
||||
size: ScreenAdaper.height(40),
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
title: Text(
|
||||
title,
|
||||
style: TextStyle(fontSize: ScreenAdaper.height(30)),
|
||||
|
|
|
@ -576,6 +576,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.0"
|
||||
math_expressions:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: math_expressions
|
||||
sha256: "3576593617c3870d75728a751f6ec6e606706d44e363f088ac394b5a28a98064"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.0"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
@ -65,6 +65,7 @@ dependencies:
|
|||
flutter_sticky_header: ^0.6.5
|
||||
flutter_slidable: ^3.1.0
|
||||
pinyin: ^3.2.0
|
||||
math_expressions: ^2.4.0
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
|
Loading…
Reference in New Issue