Browse Source

生化csv数据解析、呈现、筛选

loki.wu 11 months ago
parent
commit
8903043ac1

+ 90 - 0
lib/managers/csvDataManager.dart

@@ -0,0 +1,90 @@
+import 'dart:convert';
+
+import 'package:fis_common/logger/logger.dart';
+import 'package:flutter/services.dart';
+import 'package:get/get.dart';
+
+import 'interfaces/csvData.dart';
+import 'interfaces/models/csv_data.dart';
+import 'interfaces/template.dart';
+
+class CsvDataManager implements ICsvDataManager {
+  ///将csv中的数据解析成结构化数据
+  @override
+  Future<List<CsvRevord>> CsvDataConvert(List<List<dynamic>> datas) async {
+    var json = await Get.find<ITemplateManager>()
+        .getTemplateByKey("BiochemicalValues");
+    List<CsvRevord> csvRevords = [];
+    List<dynamic> items = jsonDecode(json);
+    List<List<List<dynamic>>> result = _splitIntoChunks(datas);
+    for (List<List<dynamic>> chunkDatas in result) {
+      List<CsvData> csvDatas = [];
+      for (var item in items) {
+        try {
+          if (item is Map) {
+            int row = item["row"];
+            int column = item["column"];
+            String key = item["key"];
+            String des = item["des"];
+            String identifier = item["identifier"];
+            String referenceRange = item["referenceRange"];
+            String unit = item["unit"];
+            String value = "";
+            if (chunkDatas.length >= row + 1) {
+              var rowValue = chunkDatas[row];
+              if (rowValue.length >= column + 1) {
+                value = chunkDatas[row][column].toString().trim();
+              } else {
+                continue;
+              }
+            } else {
+              continue;
+            }
+            csvDatas.add(CsvData(
+              key: key,
+              value: value,
+              des: des,
+              unit: unit,
+              identifier: identifier,
+              referenceRange: referenceRange,
+            ));
+          }
+        } catch (e) {
+          logger.e("CsvDataManager csvDataConvert ex:", e);
+        }
+      }
+      csvRevords.add(CsvRevord(csvDatas));
+    }
+    return csvRevords;
+  }
+
+  List<List<List<dynamic>>> _splitIntoChunks(List<List<dynamic>> datas) {
+    List<List<List<dynamic>>> records = [];
+    List<List<dynamic>> currentRecord = [];
+
+    for (int i = 0; i < datas.length; i++) {
+      // Check if the current element and the next one are empty lists
+      if ((datas[i].length == 1 && datas[i][0].isEmpty) &&
+          i + 1 < datas.length &&
+          (datas[i + 1].length == 1 && datas[i + 1][0].isEmpty)) {
+        // Skip the next empty list as it is part of the separator
+        i++;
+        // Add the current record to records if it is not empty
+        if (currentRecord.isNotEmpty) {
+          records.add(List.from(currentRecord));
+          currentRecord.clear();
+        }
+      } else {
+        // Add the current data to the current record
+        currentRecord.add(datas[i]);
+      }
+    }
+
+    // Add the last record if it's not empty (case when no trailing empty lists)
+    if (currentRecord.isNotEmpty) {
+      records.add(currentRecord);
+    }
+
+    return records;
+  }
+}

+ 3 - 0
lib/managers/index.dart

@@ -52,12 +52,14 @@ import 'package:vitalapp/managers/upgrade.dart';
 import 'application.dart';
 import 'appointment.dart';
 import 'cache.dart';
+import 'csvDataManager.dart';
 import 'data_convert.dart';
 import 'interfaces/application.dart';
 import 'interfaces/appointment.dart';
 import 'interfaces/base.dart';
 import 'interfaces/cache.dart';
 import 'interfaces/contract_template.dart';
+import 'interfaces/csvData.dart';
 import 'interfaces/language.dart';
 import 'interfaces/record_data_cache.dart';
 import 'interfaces/report.dart';
@@ -83,6 +85,7 @@ abstract class ManagerCenter {
     Get.put<IServicePackManager>(ServicePackManager());
     Get.put<IContractTemplateManager>(ContractTemplateManager());
     Get.put<IContractManager>(ContractManager());
+    Get.put<ICsvDataManager>(CsvDataManager());
     Get.put<IExamManager>(ExamManager());
     Get.put<IFollowUpManager>(FollowUpManager());
     Get.put<ITemplateManager>(TemplateManager());

+ 6 - 0
lib/managers/interfaces/csvData.dart

@@ -0,0 +1,6 @@
+import 'base.dart';
+import 'models/csv_data.dart';
+
+abstract class ICsvDataManager implements IManager {
+  Future<List<CsvRevord>> CsvDataConvert(List<List<dynamic>> datas);
+}

+ 28 - 0
lib/managers/interfaces/models/csv_data.dart

@@ -0,0 +1,28 @@
+class CsvData {
+  String value;
+
+  String key;
+
+  String? des;
+
+  String? unit;
+
+  String? identifier;
+
+  String? referenceRange;
+
+  CsvData({
+    required this.value,
+    required this.key,
+    this.des,
+    this.unit,
+    this.identifier,
+    this.referenceRange,
+  });
+}
+
+class CsvRevord {
+  List<CsvData> csvDatas;
+
+  CsvRevord(this.csvDatas);
+}

+ 4 - 0
lib/pages/home/widgets/avatar.dart

@@ -1,3 +1,7 @@
+import 'dart:async';
+import 'dart:convert';
+
+import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
 import 'package:get/get.dart';
 import 'package:vitalapp/architecture/utils/prompt_box.dart';

+ 303 - 0
lib/pages/patient/csv_datas/csv_datas_view.dart

@@ -0,0 +1,303 @@
+import 'package:fis_common/index.dart';
+import 'package:flutter/material.dart';
+import 'package:get/get.dart';
+import 'package:vitalapp/components/appbar.dart';
+import 'package:vitalapp/components/search_input.dart';
+import 'package:vitalapp/managers/interfaces/models/csv_data.dart';
+
+class CSVDatasView extends StatefulWidget {
+  final List<CsvRevord> records;
+
+  CSVDatasView(this.records);
+
+  @override
+  State<StatefulWidget> createState() {
+    return CSVDatasViewState();
+  }
+}
+
+class CSVDatasViewState extends State<CSVDatasView> {
+  final _searchController = TextEditingController();
+  List<CsvRevord> _pendingUploadDatas = [];
+  List<CsvRevord> _errorDatas = [];
+  List<String> _selectedCsvDataCodes = [];
+  bool _isShowErrorData = false;
+  bool _isSelectedAll = true;
+
+  @override
+  void initState() {
+    //sampleBarcode对应的就是体检系统的条码
+    _pendingUploadDatas = widget.records
+        .where((element) =>
+            element.csvDatas
+                .firstWhereOrNull((element) => element.key == "sampleBarcode")
+                ?.value
+                .isNotNullOrEmpty ??
+            false)
+        .toList();
+    _errorDatas = widget.records
+        .where((element) =>
+            element.csvDatas
+                .firstWhereOrNull((element) => element.key == "sampleBarcode")
+                ?.value
+                .isNullOrEmpty ??
+            false)
+        .toList();
+    _selectedAll();
+    super.initState();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      backgroundColor: Colors.white,
+      appBar: VAppBar(
+        titleWidget: Text(
+          "数据上传",
+          style: const TextStyle(fontSize: 24, color: Colors.white),
+        ),
+        actions: [
+          if (!_isShowErrorData)
+            TextButton.icon(
+              onPressed: () async {},
+              label: Text(
+                '提交',
+                style: TextStyle(fontSize: 20, color: Colors.white),
+              ),
+              icon: Icon(Icons.save, size: 32, color: Colors.white),
+            ),
+        ],
+      ),
+      body: _buildBody(),
+    );
+  }
+
+  Widget _buildBody() {
+    return Column(
+      children: [
+        Container(
+          height: 40,
+          child: _buildHead(),
+          margin: EdgeInsets.symmetric(vertical: 10),
+        ),
+        Expanded(
+          child: ListView(
+            children: [
+              if (_isShowErrorData) ...[
+                ..._errorDatas.map((e) => _buildRecord(e)),
+              ] else ...[
+                ..._pendingUploadDatas.map((e) => _buildRecord(e)),
+              ],
+            ],
+          ),
+        ),
+      ],
+    );
+  }
+
+  Widget _buildRecord(CsvRevord e) {
+    List<CsvData> csvDatas = e.csvDatas;
+    List<Widget> children = [];
+    for (var c in csvDatas) {
+      children.add(_buildItem(c));
+    }
+    var code = csvDatas
+            .firstWhereOrNull((element) => element.key == "sampleBarcode")
+            ?.value ??
+        '';
+    return Container(
+      padding: EdgeInsets.only(left: 10),
+      width: MediaQuery.of(context).size.width,
+      child: Row(
+        children: [
+          if (!_isShowErrorData)
+            Checkbox(
+              value: _selectedCsvDataCodes.contains(code),
+              onChanged: (v) {
+                if (_selectedCsvDataCodes.contains(code)) {
+                  _selectedCsvDataCodes.remove(code);
+                } else {
+                  _selectedCsvDataCodes.add(code);
+                }
+                setState(() {});
+              },
+            ),
+          Expanded(
+            child: Wrap(
+              children: children,
+            ),
+          ),
+        ],
+      ),
+      decoration: BoxDecoration(
+          border: Border(
+        bottom: BorderSide(
+          color: Colors.grey[300]!, // 边框颜色
+          width: 1.0, // 边框宽度
+        ),
+      )),
+    );
+  }
+
+  Widget _buildItem(CsvData c) {
+    if (c.value.isEmpty) {
+      return SizedBox();
+    }
+    String value = c.value;
+    if (c.unit.isNotNullOrEmpty) {
+      value += " " + c.unit!;
+    }
+    String title = c.des ?? '';
+    if (title.isNotEmpty) {
+      title += ":";
+    }
+    return Row(
+      mainAxisSize: MainAxisSize.min,
+      children: [
+        Text(
+          title,
+          style: TextStyle(fontWeight: FontWeight.bold),
+        ),
+        SizedBox(
+          width: 3,
+        ),
+        Text(value),
+        SizedBox(
+          width: 15,
+        ),
+      ],
+    );
+  }
+
+  Widget _buildHead() {
+    return Row(
+      children: [
+        SizedBox(width: 10),
+        if (!_isShowErrorData)
+          Checkbox(
+            value: _isSelectedAll,
+            onChanged: (v) {
+              _isSelectedAll = !_isSelectedAll;
+              _selectedCsvDataCodes.clear();
+              if (_isSelectedAll) {
+                _selectedAll();
+              }
+              setState(() {});
+            },
+          ),
+        _buildSearchInput(),
+        SizedBox(width: 20),
+        _buildTips(),
+        SizedBox(width: 10),
+        _buildSwichButton(),
+        if (_isShowErrorData) ...[
+          SizedBox(width: 10),
+          Icon(
+            Icons.warning,
+            color: Colors.orange,
+          ),
+          Text(
+            "请检查样本条码是否正确!",
+            style: TextStyle(color: Colors.orange),
+          ),
+        ],
+      ],
+    );
+  }
+
+  void _selectedAll() {
+    for (var data in _pendingUploadDatas) {
+      var code = data.csvDatas
+              .firstWhereOrNull((element) => element.key == "sampleBarcode")
+              ?.value ??
+          '';
+      _selectedCsvDataCodes.add(code);
+    }
+  }
+
+  Widget _buildSearchInput() {
+    return Container(
+      width: 400,
+      margin: EdgeInsets.only(left: 20),
+      child: VSearchInput(
+        textEditingController: _searchController,
+        placeholder: "请输入姓名",
+        onSearch: (v) {
+          if (_isShowErrorData) {
+            var errorDatas = widget.records
+                .where((element) =>
+                    element.csvDatas
+                        .firstWhereOrNull(
+                            (element) => element.key == "sampleBarcode")
+                        ?.value
+                        .isNullOrEmpty ??
+                    false)
+                .toList();
+            _errorDatas = errorDatas
+                .where((element) =>
+                    element.csvDatas
+                        .firstWhereOrNull(
+                            (element) => element.key == "patientName")
+                        ?.value
+                        .contains(v) ??
+                    false)
+                .toList();
+          } else {
+            var pendingUploadDatas = widget.records
+                .where((element) =>
+                    element.csvDatas
+                        .firstWhereOrNull(
+                            (element) => element.key == "sampleBarcode")
+                        ?.value
+                        .isNotNullOrEmpty ??
+                    false)
+                .toList();
+            _pendingUploadDatas = pendingUploadDatas
+                .where((element) =>
+                    element.csvDatas
+                        .firstWhereOrNull(
+                            (element) => element.key == "patientName")
+                        ?.value
+                        .contains(v) ??
+                    false)
+                .toList();
+            if (_isSelectedAll) {
+              _selectedCsvDataCodes.clear();
+              for (var data in _pendingUploadDatas) {
+                var code = data.csvDatas
+                        .firstWhereOrNull(
+                            (element) => element.key == "sampleBarcode")
+                        ?.value ??
+                    '';
+                _selectedCsvDataCodes.add(code);
+              }
+            }
+          }
+          setState(() {});
+        },
+      ),
+    );
+  }
+
+  Widget _buildTips() {
+    var tips =
+        "共计 ${widget.records.length} 条数据,其中校验通过 ${_pendingUploadDatas.length} 条数据";
+    if (_errorDatas.length > 0) {
+      tips += ",校验不通过${_errorDatas.length}条数据";
+    }
+    return Text(tips);
+  }
+
+  Widget _buildSwichButton() {
+    return OutlinedButton(
+      onPressed: () {
+        setState(() {
+          _isShowErrorData = !_isShowErrorData;
+        });
+      },
+      child: Text(
+        _isShowErrorData ? "切换至校验通过数据" : "切换至校验不通过数据",
+      ),
+    );
+  }
+}

+ 82 - 0
lib/pages/patient/csv_datas/import_datas_view.dart

@@ -0,0 +1,82 @@
+import 'dart:async';
+import 'dart:convert';
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:get/get.dart';
+import 'package:vitalapp/architecture/utils/prompt_box.dart';
+import 'package:vitalapp/pages/widgets/function_button.dart';
+import 'package:universal_html/html.dart' as html;
+import 'package:csv/csv.dart';
+import 'package:vitalapp/managers/interfaces/csvData.dart';
+import 'package:vitalapp/pages/patient/csv_datas/csv_datas_view.dart';
+import 'package:vitalapp/store/store.dart';
+
+class ImportDataView extends StatelessWidget {
+  @override
+  Widget build(BuildContext context) {
+    return GridView.count(
+      crossAxisCount: 7, // 每行显示的子组件数量
+      crossAxisSpacing: 4.0, // 子组件之间的水平间距
+      mainAxisSpacing: 4.0, // 子组件之间的垂直间距
+      childAspectRatio: 1.0, // 子组件的宽高比
+      children: [
+        FunctionButton(
+          label: "生化数据",
+          icon: Icon(
+            Icons.abc,
+            size: 40,
+          ),
+          onTap: _csvImportClick,
+        ),
+      ],
+    );
+  }
+
+  Future<void> _csvImportClick() async {
+    final html.FileUploadInputElement uploadInput =
+        html.FileUploadInputElement();
+    uploadInput.accept = '.xls,.xlsx,.csv'; // 设置接受的文件类型
+    uploadInput.click();
+
+    await uploadInput.onChange.first;
+
+    html.File? file = uploadInput.files?.first;
+    String extension = file?.name.split('.').last.toLowerCase() ?? "";
+    if (extension == "csv") {
+      Store.app.busy = true;
+      try {
+        List<List<dynamic>> csvData = await readCSVFile(file!);
+        var records = await Get.find<ICsvDataManager>().CsvDataConvert(csvData);
+        if (records.isNotEmpty) {
+          Get.to(CSVDatasView(records));
+        } else {
+          PromptBox.toast('无可上传数据');
+        }
+      } catch (e) {}
+      Store.app.busy = false;
+    }
+  }
+
+  Future<List<List<dynamic>>> readCSVFile(html.File file) async {
+    final reader = html.FileReader();
+    final completer = Completer<List<List<dynamic>>>();
+
+    reader.onLoadEnd.listen((e) {
+      // 尝试将内容转换为UTF-8编码的字符串
+      String contents;
+      try {
+        contents = utf8.decode(reader.result as List<int>);
+      } catch (e) {
+        // 如果转换失败,可能是因为结果不是List<int>,尝试直接转换为字符串
+        contents = reader.result.toString();
+      }
+      final List<List<dynamic>> csvData =
+          CsvToListConverter().convert(contents);
+      completer.complete(csvData);
+    });
+
+    reader.readAsText(file);
+    return completer.future;
+  }
+}

+ 11 - 1
lib/pages/patient/detail/widgets/functions_panel.dart

@@ -1,9 +1,14 @@
+import 'dart:async';
+import 'dart:convert';
+
 import 'package:fis_jsonrpc/rpc.dart';
 import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
 import 'package:get/get.dart';
+import 'package:vitalapp/architecture/utils/prompt_box.dart';
 import 'package:vitalapp/architecture/values/features.dart';
 import 'package:vitalapp/consts/styles.dart';
+import 'package:vitalapp/managers/interfaces/csvData.dart';
 import 'package:vitalapp/managers/interfaces/models/crowd_labels.dart';
 import 'package:vitalapp/pages/check/prescription/Iodine_deficiency_disorder.dart';
 import 'package:vitalapp/pages/check/prescription/blood_sugar_disorder.dart';
@@ -17,10 +22,15 @@ import 'package:vitalapp/pages/check/prescription/trichomoniasis_vaginitis.dart'
 import 'package:vitalapp/pages/check/prescription/psychiatric_prescription.dart';
 import 'package:vitalapp/pages/check/prescription/tuberculosis_prescription.dart';
 import 'package:vitalapp/pages/check/prescription/under_fiveMalnutrition_prescription.dart';
+import 'package:vitalapp/pages/patient/csv_datas/csv_datas_view.dart';
 import 'package:vitalapp/pages/widgets/function_button.dart';
 import 'package:vitalapp/store/store.dart';
-
+import 'package:universal_html/html.dart' as html;
 import '../controller.dart';
+import 'package:csv/csv.dart';
+import 'package:vitalapp/managers/interfaces/csvData.dart';
+import 'package:vitalapp/pages/patient/csv_datas/csv_datas_view.dart';
+import 'package:vitalapp/store/store.dart';
 
 /// 功能入口面板
 class FunctionsPanel extends GetView<PatientDetailController> {

+ 15 - 0
lib/pages/settings/center/view.dart

@@ -8,6 +8,7 @@ import 'package:vitalapp/components/appbar.dart';
 import 'package:vitalapp/components/side_nav/defines.dart';
 import 'package:vitalapp/components/side_nav/side_nav.dart';
 import 'package:vitalapp/global.dart';
+import 'package:vitalapp/pages/patient/csv_datas/import_datas_view.dart';
 import 'package:vitalapp/pages/settings/about/view.dart';
 import 'package:vitalapp/pages/settings/devices/controller.dart';
 import 'package:vitalapp/pages/settings/devices/view.dart';
@@ -37,6 +38,10 @@ class SettingCenterPage extends GetView<SettingCenterController> {
     if (!kIsWeb) {
       items.add(_buildDevicesSettingItem());
       items.add(_buildDataSyncItem());
+    } else {
+      if (kDebugMode) {
+        items.add(_buildDataImport());
+      }
     }
     items.add(_buildServerSettingItem());
     items.add(_buildAboutItem());
@@ -204,6 +209,16 @@ class SettingCenterPage extends GetView<SettingCenterController> {
       child: child,
     );
   }
+
+  VSideNavMenuItem _buildDataImport() {
+    return VSideNavMenuItem(
+      title: "数据导入",
+      icon: Icon(Icons.download, color: Colors.grey.shade700),
+      pageBuilder: (_) => _buildInterval(
+        ImportDataView(),
+      ),
+    );
+  }
 }
 
 class ProgressBar extends StatelessWidget {

+ 10 - 2
pubspec.lock

@@ -241,6 +241,14 @@ packages:
       url: "https://pub.flutter-io.cn"
     source: hosted
     version: "0.17.2"
+  csv:
+    dependency: "direct overridden"
+    description:
+      name: csv
+      sha256: c6aa2679b2a18cb57652920f674488d89712efaf4d3fdf2e537215b35fc19d6c
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "6.0.0"
   cupertino_icons:
     dependency: transitive
     description:
@@ -383,8 +391,8 @@ packages:
     dependency: "direct main"
     description:
       path: "."
-      ref: aa692c0
-      resolved-ref: aa692c0e29b264675affaa4be4d03d0d75204498
+      ref: a08de07
+      resolved-ref: a08de07daead4ffca86b693cb5da958509285459
       url: "http://git.ius.plus:88/Project-Wing/fis_lib_jsonrpc.git"
     source: git
     version: "0.0.1"

+ 2 - 1
pubspec.yaml

@@ -146,6 +146,7 @@ dependency_overrides:
   flutter_svg: 2.0.2
   audio_session: 0.1.6
   flutter_sound: 9.2.13
+  csv: 6.0.0
   flutter_sound_platform_interface: 9.2.13
   universal_html: 2.0.8
 
@@ -160,7 +161,7 @@ dependency_overrides:
   fis_jsonrpc:
     git:
       url: http://git.ius.plus:88/Project-Wing/fis_lib_jsonrpc.git
-      ref: aa692c0
+      ref: a08de07
     #path: ../fis_lib_jsonrpc
   fis_theme:
     git: