diff --git a/lib/logistics.dart b/lib/logistics.dart index 75e7c1e..f25a8de 100644 --- a/lib/logistics.dart +++ b/lib/logistics.dart @@ -1,7 +1,12 @@ +import 'dart:convert'; + +import 'package:async/async.dart'; import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:go_router/go_router.dart'; import 'package:logistics_tools/utils/request.dart'; +import 'package:uuid/uuid.dart'; class Logistics extends StatefulWidget { const Logistics({super.key}); @@ -12,11 +17,25 @@ class Logistics extends StatefulWidget { class _LogisticsState extends State { List goodsList = []; + String _orderPayWay = ""; + List _packages = []; + int _activePackage = 0; + final TextEditingController _goodsNameController = + TextEditingController(text: ""); + final TextEditingController _goodsSpecController = + TextEditingController(text: ""); + final TextEditingController _goodsNumController = TextEditingController(); + final _memoizer = AsyncMemoizer(); @override void initState() { // TODO: implement initState super.initState(); + setState(() { + _packages = [ + {"id": const Uuid().v4(), "logisticsNumber": "", "goods": []} + ]; + }); } Future fetchGoodsList(BuildContext context) async { @@ -24,13 +43,16 @@ class _LogisticsState extends State { GoRouterState.of(context).uri.queryParameters["orderNumber"]; String? orderPayWay = GoRouterState.of(context).uri.queryParameters["orderPayWay"]; - print(orderPayWay); + _orderPayWay = orderPayWay!; + if (orderNumber?.isEmpty ?? true) { context.pop(); } - Response response = await dio.get( - "https://www.sanpinhuicai.com/wisdommining/api/order/goodsOfOrder", - queryParameters: {"orderNumber": orderNumber}); + Response response = await _memoizer.runOnce(() async { + return await dio.get( + "https://www.sanpinhuicai.com/wisdommining/api/order/goodsOfOrder", + queryParameters: {"orderNumber": orderNumber}); + }); goodsList = response.data["value"]; return response; } @@ -39,15 +61,55 @@ class _LogisticsState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: const Text("填写物流信息"), + title: Text( + "填写物流信息 (${GoRouterState.of(context).uri.queryParameters["orderPayWay"]})"), + actions: [ + ElevatedButton( + onPressed: () { + // 数据验证 + if (_orderPayWay != "套餐商品") { + bool hasUnAddedGoods = + goodsList.every((el) => el["goodsNum"] > 0); + if (hasUnAddedGoods) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + content: Text("有未添加至包裹的商品"), + backgroundColor: Colors.redAccent, + )); + return; + } + } + bool result = _packages.every((package) { + return package["logisticsNumber"].toString().isNotEmpty && + package["goods"].length > 0 && + package["goods"].every((goods) { + return goods["name"].toString().isNotEmpty && + goods["num"].toString().isNotEmpty && + goods["spec"].toString().isNotEmpty; + }); + }); + if (!result) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + backgroundColor: Colors.redAccent, + content: Text("有未填写的数据"))); + return; + } + String logisticsJson = jsonEncode(_packages); + context.pop(logisticsJson); + }, + child: const Row( + children: [Text("保存"), Icon(Icons.save)], + )), + const SizedBox( + width: 4, + ) + ], ), body: Container( padding: const EdgeInsets.all(40), width: double.infinity, child: FutureBuilder( future: fetchGoodsList(context), - builder: - (BuildContext context, AsyncSnapshot snapshot) { + builder: (BuildContext context, AsyncSnapshot snapshot) { if (snapshot.hasData) { return Card( color: Colors.white, @@ -57,27 +119,338 @@ class _LogisticsState extends State { children: [ Expanded( flex: 1, - child: ListView.builder( - itemCount: goodsList.length, - itemBuilder: (context, index) { - final goods = goodsList[index]; - return ListTile( - trailing: IconButton( - icon: const Icon(Icons.add), - onPressed: () {}, - ), - leading: Image.network( - goods["goodsPhoto"], - width: 100, - height: 100, - ), - title: Text( - "${goods["goodsName"]} - ${goods?["wisdGoodsSpec"]?["specName"] ?? ''}"), - subtitle: - Text("数量 : ${goods?["goodsNum"]}"), - ); - })), - const Expanded(flex: 1, child: Placeholder()) + child: _orderPayWay == "套餐商品" + ? Column( + children: [ + Card( + color: Colors.white70, + child: Row( + children: [ + Image.network( + width: 80, + height: 80, + goodsList[0]["goodsPhoto"]), + const SizedBox( + width: 16, + ), + Text( + "${goodsList[0]["goodsName"]}") + ], + ), + ), + const SizedBox( + height: 20, + ), + TextField( + controller: _goodsNameController, + decoration: const InputDecoration( + labelText: "商品名称", + border: OutlineInputBorder()), + ), + const SizedBox( + height: 20, + ), + TextField( + controller: _goodsSpecController, + decoration: const InputDecoration( + labelText: "商品规格", + border: OutlineInputBorder()), + ), + const SizedBox( + height: 20, + ), + TextField( + controller: _goodsNumController, + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter + .digitsOnly + ], + decoration: const InputDecoration( + labelText: "商品数量", + border: OutlineInputBorder()), + ), + const SizedBox( + height: 20, + ), + ElevatedButton( + onPressed: () { + if (_goodsNumController + .text.isEmpty || + _goodsNameController + .text.isEmpty || + _goodsSpecController + .text.isEmpty) { + ScaffoldMessenger.of(context) + .showSnackBar( + const SnackBar( + content: Text( + "请填写完整商品信息"))); + return; + } + int goodsNum = int.parse( + _goodsNumController.text); + String goodsName = + _goodsNameController.text; + String goodsSpec = + _goodsSpecController.text; + var goods = { + "id": const Uuid().v4(), + "name": goodsName, + "num": goodsNum, + "specId": const Uuid().v4(), + "spec": goodsSpec + }; + var packagesList = _packages; + + packagesList[_activePackage] + ["goods"] + .add(goods); + setState(() { + _packages = packagesList; + }); + _goodsNumController.clear(); + _goodsNameController.clear(); + _goodsSpecController.clear(); + }, + child: const Text("添加到包裹")) + ], + ) + : ListView.builder( + itemCount: goodsList + .where((el) => el["goodsNum"] > 0) + .length, + itemBuilder: (context, index) { + final goods = goodsList + .where((el) => el["goodsNum"] > 0) + .toList()[index]; + return ListTile( + trailing: IconButton( + icon: const Icon(Icons.add), + onPressed: () { + // 检查商品及规格是否已存在于目标包裹 + int targetIndex = _packages[ + _activePackage]["goods"] + .indexWhere((el) => + el["id"] == + goods["id"] && + goods["wisdGoodsSpec"] + ["id"] == + el["specId"]); + if (targetIndex != -1) { + setState(() { + _packages[_activePackage] + ["goods"] + [targetIndex] + ["num"] += 1; + }); + } else { + setState(() { + _packages[_activePackage] + ["goods"] + .add({ + "id": goods["id"], + "specId": + goods["wisdGoodsSpec"] + ["id"], + "name": + goods["goodsName"], + "spec": + goods?["wisdGoodsSpec"] + ?[ + "specName"] ?? + "", + "num": 1 + }); + }); + } + var curGoods = + goodsList.firstWhere( + (el) => + el["id"] == + goods["id"], + orElse: () => -1); + + setState(() { + curGoods["goodsNum"] -= 1; + }); + }, + ), + leading: Image.network( + goods["goodsPhoto"], + width: 100, + height: 100, + ), + title: Text( + "${goods["goodsName"]} - ${goods?["wisdGoodsSpec"]?["specName"] ?? ''}"), + subtitle: Text( + "数量 : ${goods?["goodsNum"]}"), + ); + })), + const SizedBox( + width: 20, + ), + Expanded( + flex: 1, + child: Column( + children: [ + Expanded( + child: ListView.builder( + itemCount: _packages.length, + itemBuilder: + (BuildContext context, int index) { + return PackageCard( + onLogisticsChanged: (value) { + _packages[index] + ["logisticsNumber"] = + value; + }, + onTap: () { + setState(() { + _activePackage = index; + }); + }, + isActive: + _activePackage == index, + package: _packages[index], + index: index) + /*GestureDetector( + onTap: () { + setState(() { + _activePackage = index; + }); + }, + child: Card( + elevation: _activePackage == index + ? 5 + : null, + shape: _activePackage == index + ? RoundedRectangleBorder( + borderRadius: + BorderRadius.circular( + 12.0), + side: const BorderSide( + color: Colors.blue, + width: 1.0, + ), + ) + : null, + child: Stack( + // clipBehavior: Clip.none, + children: [ + const SizedBox( + height: 100, + ), + Padding( + padding: + const EdgeInsets.fromLTRB( + 20, 30, 20, 20), + child: Column( + crossAxisAlignment: + CrossAxisAlignment + .stretch, + children: [ + TextField( + decoration: + const InputDecoration( + border: + OutlineInputBorder(), + labelText: + "物流单号"), + ), + const SizedBox( + height: 20, + ), + DataTable(columns: const [ + DataColumn( + label: Text("名称")), + DataColumn( + label: Text("规格")), + DataColumn( + label: Text("数量")) + ], rows: [ + for (var goodsItem + in _packages[index] + ["goods"]) + DataRow(cells: [ + DataCell(Text( + "${goodsItem["name"]}")), + DataCell(Text( + "${goodsItem["spec"]}")), + DataCell(Text( + "${goodsItem["num"]}")), + ]) + ]), + ], + ), + ), + Positioned( + left: 20, + top: 2, + child: Text("包裹${index + 1}"), + ), + _packages.length > 1 + ? Positioned( + top: 2, + right: 2, + child: SizedBox( + width: 20, + height: 20, + child: IconButton( + color: Colors + .redAccent, + iconSize: 20, + padding: + EdgeInsets.zero, + icon: const Icon( + Icons.close), + onPressed: () { + var curId = + _packages[ + index] + ["id"]; + setState(() { + _activePackage = + _activePackage >= + index + ? _activePackage - + 1 + : _activePackage; + _packages = _packages + .where((el) => + el["id"] != + curId) + .toList(); + }); + }, + ), + )) + : const SizedBox( + width: 0, + ) + ], + ), + ), + )*/ + ; + }, + )), + ElevatedButton( + onPressed: () { + setState(() { + _packages = [ + ..._packages, + { + "logisticsNumber": "", + "id": const Uuid().v4(), + "goods": [] + } + ]; + }); + }, + child: const Text("添加包裹")) + ], + )) /* GridView.builder( gridDelegate: @@ -107,3 +480,118 @@ class _LogisticsState extends State { ))); } } + +class PackageCard extends StatefulWidget { + const PackageCard({ + super.key, + required this.onTap, + required this.isActive, + required this.package, + required this.index, + required this.onLogisticsChanged, + }); + + final bool isActive; + final Function() onTap; + final Map package; + final int index; + final Function(String) onLogisticsChanged; + + @override + State createState() => _PackageCardState(); +} + +class _PackageCardState extends State { + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: widget.onTap, + /* () { + setState(() { + _activePackage = index; + }); + },*/ + child: Card( + elevation: widget.isActive ? 5 : null, + shape: widget.isActive + ? RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.0), + side: const BorderSide( + color: Colors.blue, + width: 1.0, + ), + ) + : null, + child: Stack( + // clipBehavior: Clip.none, + children: [ + const SizedBox( + height: 100, + ), + Padding( + padding: const EdgeInsets.fromLTRB(20, 30, 20, 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + TextField( + onChanged: widget.onLogisticsChanged, + decoration: const InputDecoration( + border: OutlineInputBorder(), labelText: "物流单号"), + ), + const SizedBox( + height: 20, + ), + DataTable(columns: const [ + DataColumn(label: Text("名称")), + DataColumn(label: Text("规格")), + DataColumn(label: Text("数量")) + ], rows: [ + for (var goodsItem in widget.package["goods"]) + DataRow(cells: [ + DataCell(Text("${goodsItem["name"]}")), + DataCell(Text("${goodsItem["spec"]}")), + DataCell(Text("${goodsItem["num"]}")), + ]) + ]), + ], + ), + ), + Positioned( + left: 20, + top: 2, + child: Text("包裹${widget.index + 1}"), + ), + /* _packages.length > 1 + ? Positioned( + top: 2, + right: 2, + child: SizedBox( + width: 20, + height: 20, + child: IconButton( + color: Colors.redAccent, + iconSize: 20, + padding: EdgeInsets.zero, + icon: const Icon(Icons.close), + onPressed: () { + var curId = _packages[index]["id"]; + setState(() { + _activePackage = _activePackage >= index + ? _activePackage - 1 + : _activePackage; + _packages = _packages + .where((el) => el["id"] != curId) + .toList(); + }); + }, + ), + )) + : const SizedBox( + width: 0, + )*/ + ], + ), + ), + ); + } +} diff --git a/lib/order_list.dart b/lib/order_list.dart index a740652..b2a1857 100644 --- a/lib/order_list.dart +++ b/lib/order_list.dart @@ -2,10 +2,9 @@ import 'dart:io'; import 'package:excel/excel.dart'; import 'package:file_picker/file_picker.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:go_router/go_router.dart'; -import 'package:path_provider/path_provider.dart'; class OrderList extends StatefulWidget { const OrderList({super.key}); @@ -16,8 +15,8 @@ class OrderList extends StatefulWidget { class _OrderListState extends State { final List lst = []; - List headers = []; - List> data = []; + List _headers = []; + List> _data = []; late Excel _excel; ScrollController scrollController = ScrollController(); final TextEditingController _logComController = @@ -28,45 +27,54 @@ class _OrderListState extends State { return Scaffold( appBar: AppBar( backgroundColor: Colors.white, - title: const Text("test"), + title: const Text("订单列表"), actions: [ - IconButton( + ElevatedButton( onPressed: () { importExcelFile(); }, - icon: const Icon(Icons.add_circle)), - IconButton( + child: const Row( + children: [Icon(Icons.add_circle), Text("导入")], + )), + const SizedBox( + width: 4, + ), + ElevatedButton( onPressed: () { exportExcelFile(); }, - icon: const Icon(Icons.output)) + child: const Row( + children: [Icon(CupertinoIcons.download_circle), Text("导出")], + )), + const SizedBox( + width: 4, + ), ], ), body: Container( width: double.infinity, - // color: Colors.white, padding: const EdgeInsets.all(20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Card( - color: Colors.white, - margin: EdgeInsets.zero, - child: headers.isNotEmpty - ? /*Scrollbar( + child: _headers.isNotEmpty + ? Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Card( + color: Colors.white, + margin: EdgeInsets.zero, + child: /*Scrollbar( // controller: scrollController, child:*/ - SingleChildScrollView( + SingleChildScrollView( scrollDirection: Axis.horizontal, padding: const EdgeInsets.all(12.0), child: DataTable( - columns: headers.map((el) { + columns: _headers.map((el) { return DataColumn(label: Text(el)); }).toList(), - rows: data.asMap().entries.map((row) { + rows: _data.asMap().entries.map((row) { return DataRow( cells: row.value.asMap().entries.map((col) { - String colTitle = headers[col.key]; + String colTitle = _headers[col.key]; return DataCell(colTitle == "物流名称" ? Row( children: [ @@ -76,7 +84,7 @@ class _OrderListState extends State { iconSize: 12, onPressed: () { _logComController.text = - data[row.key][col.key] + _data[row.key][col.key] .toString(); showDialog( context: context, @@ -122,13 +130,13 @@ class _OrderListState extends State { Text("请输入物流公司"))); return; } - final _data = - data; - data[row.key][col - .key] = + final data = + _data; + _data[row.key][ + col.key] = logisticsCom; setState(() { - data = _data; + _data = data; }); context.pop(); }, @@ -141,58 +149,91 @@ class _OrderListState extends State { ], ) : colTitle == "物流信息" - ? ElevatedButton( - onPressed: () { - String orderNumber = - data[row.key][2].toString(); - String orderPayWay = - data[row.key][15].toString(); - if (orderPayWay.isEmpty || - orderNumber.isEmpty) { - ScaffoldMessenger.of(context) - .showSnackBar(const SnackBar( - content: Text("该行数据有误"))); - return; - } - context.pushNamed("logistics", - queryParameters: { - "orderNumber": data[row.key] - [2] - .toString(), - "orderPayWay": data[row.key] - [15] - .toString(), + ? Row( + children: [ + _data[row.key][col.key] + .toString() + .isNotEmpty + ? SizedBox( + width: 120, + child: Text( + _data[row.key][col.key].toString(), + overflow: + TextOverflow.ellipsis, + ), + ) + : const SizedBox(), + ElevatedButton( + onPressed: () { + String orderNumber = + _data[row.key][2] + .toString(); + String orderPayWay = + _data[row.key][15] + .toString(); + if (orderPayWay.isEmpty || + orderNumber.isEmpty) { + ScaffoldMessenger.of(context) + .showSnackBar( + const SnackBar( + content: Text( + "该行数据有误"))); + return; + } + context.pushNamed("logistics", + queryParameters: { + "orderNumber": + _data[row.key][2] + .toString(), + "orderPayWay": + _data[row.key][15] + .toString(), + }).then((value) { + if (value != null) { + var newData = _data; + newData[row.key][col.key] = + value; + setState(() { + _data = newData; + }); + } }); - }, - child: const Text("填写"), + }, + child: const Text("填写"), + ) + ], ) : Text("${col.value}")); }).toList()); }).toList(), ), ) - /*)*/ - : const Text("empty"), - ) - ], - ), + /*)*/ + , + ) + ], + ) + : const Center( + child: Icon(CupertinoIcons.cloud_upload), + ), )); } Future importExcelFile() async { FilePickerResult? result = await FilePicker.platform .pickFiles(type: FileType.custom, allowedExtensions: ["xlsx", "xls"]); + if (result != null) { File file = File(result.files.single.path!); var bytes = file.readAsBytesSync(); Excel excel = Excel.decodeBytes(bytes); _excel = excel; String tableKey = excel.tables.keys.first; - List _headers = excel.tables[tableKey]?.rows.first.map((el) { + List headers = excel.tables[tableKey]?.rows.first.map((el) { return el?.value.toString() ?? ""; }).toList() ?? []; - List> _data = excel.tables[tableKey]?.rows + List> data = excel.tables[tableKey]?.rows .where((el) => (el.first?.value?.toString() ?? "").isNotEmpty) .toList() .sublist(1) @@ -203,8 +244,8 @@ class _OrderListState extends State { }).toList() ?? []; setState(() { - headers = _headers; - data = _data; + _headers = headers; + _data = data; }); } else { // 取消选择 @@ -213,7 +254,7 @@ class _OrderListState extends State { } exportExcelFile() async { - data.asMap().entries.forEach((row) { + _data.asMap().entries.forEach((row) { row.value.asMap().entries.forEach((col) { int rowIndex = row.key + 1; int colIndex = col.key; @@ -227,14 +268,19 @@ class _OrderListState extends State { }); }); var fileBytes = _excel.save(); - // var directory = await getApplicationDocumentsDirectory(); - File( - "C:\\Users\\ayaya\\Desktop\\导出订单${DateTime.now().millisecondsSinceEpoch}.xlsx") + String? directory = + await FilePicker.platform.getDirectoryPath(dialogTitle: "选择保存位置"); + if (directory == null || directory.isEmpty) { + return; + } + File("$directory\\导出订单${DateTime.now().millisecondsSinceEpoch}.xlsx") ..createSync(recursive: true) ..writeAsBytesSync(fileBytes!); - ScaffoldMessenger.of(context).showSnackBar(const SnackBar( - content: Text("导出文件成功"), - backgroundColor: Colors.green, - )); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + content: Text("导出文件成功"), + backgroundColor: Colors.green, + )); + } } } diff --git a/pubspec.lock b/pubspec.lock index 50aa833..fe4a817 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -10,7 +10,7 @@ packages: source: hosted version: "3.4.9" async: - dependency: transitive + dependency: "direct main" description: name: async sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" @@ -129,6 +129,14 @@ packages: url: "https://pub.dev" source: hosted version: "8.0.5" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" + source: hosted + version: "1.1.0" flutter: dependency: "direct main" description: flutter @@ -268,10 +276,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "9c96da072b421e98183f9ea7464898428e764bc0ce5567f27ec8693442e72514" + sha256: bca87b0165ffd7cdb9cad8edd22d18d2201e886d9a9f19b4fb3452ea7df3a72a url: "https://pub.dev" source: hosted - version: "2.2.5" + version: "2.2.6" path_provider_foundation: dependency: transitive description: @@ -349,6 +357,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" stack_trace: dependency: transitive description: @@ -397,6 +413,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.2" + uuid: + dependency: "direct main" + description: + name: uuid + sha256: "814e9e88f21a176ae1359149021870e87f7cddaf633ab678a5d2b0bff7fd1ba8" + url: "https://pub.dev" + source: hosted + version: "4.4.0" vector_math: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 804292d..5763814 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -40,6 +40,8 @@ dependencies: go_router: ^14.2.0 dio: ^5.4.3+1 path_provider: ^2.1.3 + uuid: ^4.4.0 + async: ^2.11.0 dev_dependencies: flutter_test: