在这里插入图片描述

添加药品是药箱管理应用的基础功能。用户需要录入药品的各种信息,包括名称、规格、有效期、库存等。一个设计良好的表单能够让用户快速完成药品录入,同时确保数据的完整性和准确性。

表单设计思路

添加药品页面采用分组表单的设计方式,将相关信息归类展示:基本信息库存信息有效期用法用量其他信息。这种分组方式让表单结构清晰,用户填写时不会感到混乱。

页面状态管理

首先定义页面的状态和控制器:

class _AddMedicineScreenState extends State<AddMedicineScreen> {
  final _formKey = GlobalKey<FormState>();
  final _nameController = TextEditingController();
  final _specificationController = TextEditingController();
  final _manufacturerController = TextEditingController();
  final _quantityController = TextEditingController();

  String _selectedCategory = '感冒用药';
  String _selectedUnit = '粒';
  String _selectedLocation = '客厅药箱';
  DateTime _productionDate = DateTime.now();
  DateTime _expiryDate = DateTime.now().add(const Duration(days: 365));

  final List<String> _categories = [
    '感冒用药', '解热镇痛', '抗生素', '消化系统',
    '心血管', '维生素', '外用药', '中成药', '儿童用药', '其他',
  ];

  final List<String> _units = ['粒', '片', '袋', '支', '瓶', '盒', 'ml', 'g'];
  final List<String> _locations = ['客厅药箱', '卧室药箱', '厨房药箱', '冰箱', '其他'];
}

使用GlobalKey<FormState>管理表单验证。为每个输入字段创建独立的TextEditingController,方便获取和管理输入内容。下拉选择字段使用String类型的状态变量,日期字段使用DateTime类型。预定义了分类、单位和存放位置的选项列表,用户可以从中选择,避免输入错误。

表单整体结构

页面主体使用Form包裹所有输入组件:


Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: const Text('添加药品'),
    ),
    body: Form(
      key: _formKey,
      child: SingleChildScrollView(
        padding: EdgeInsets.all(16.w),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            _buildSectionTitle('基本信息'),
            _buildTextField(
              controller: _nameController,
              label: '药品名称',
              hint: '请输入药品名称',
              required: true,
            ),
            _buildDropdown(
              label: '药品分类',
              value: _selectedCategory,
              items: _categories,
              onChanged: (value) {
                setState(() {
                  _selectedCategory = value!;
                });
              },
            ),
            // 更多字段...
            SizedBox(
              width: double.infinity,
              height: 48.h,
              child: ElevatedButton(
                onPressed: _saveMedicine,
                child: Text('保存'),
              ),
            ),
          ],
        ),
      ),
    ),
  );
}

使用SingleChildScrollView让表单可以滚动,因为字段较多可能超出屏幕高度。所有输入组件通过Column垂直排列,crossAxisAlignment.start确保左对齐。底部的保存按钮宽度设置为double.infinity,占满整行,高度48.h提供足够的点击区域。

文本输入字段

封装通用的文本输入组件:

Widget _buildTextField({
  required TextEditingController controller,
  required String label,
  String? hint,
  bool required = false,
  TextInputType keyboardType = TextInputType.text,
  int maxLines = 1,
}) {
  return TextFormField(
    controller: controller,
    keyboardType: keyboardType,
    maxLines: maxLines,
    decoration: InputDecoration(
      labelText: required ? '$label *' : label,
      hintText: hint,
      border: OutlineInputBorder(
        borderRadius: BorderRadius.circular(8.r),
      ),
      contentPadding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 12.h),
    ),
    validator: required
        ? (value) {
            if (value == null || value.isEmpty) {
              return '请输入$label';
            }
            return null;
          }
        : null,
  );
}

这个封装的组件支持多种配置:必填字段会在标签后添加星号,可以设置键盘类型(数字、文本等),支持多行输入。validator参数控制是否进行必填验证,验证失败时显示友好的错误提示。使用OutlineInputBorder提供边框样式,圆角8.r让输入框更加柔和。

下拉选择组件

下拉选择用于分类、单位等固定选项:

Widget _buildDropdown({
  required String label,
  required String value,
  required List<String> items,
  required ValueChanged<String?> onChanged,
}) {
  return DropdownButtonFormField<String>(
    value: value,
    decoration: InputDecoration(
      labelText: label,
      border: OutlineInputBorder(
        borderRadius: BorderRadius.circular(8.r),
      ),
      contentPadding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 12.h),
    ),
    items: items.map((item) {
      return DropdownMenuItem(value: item, child: Text(item));
    }).toList(),
    onChanged: onChanged,
  );
}

DropdownButtonFormField是Flutter提供的下拉选择组件,与Form表单集成良好。通过items参数传入选项列表,value参数设置当前选中值,onChanged回调处理选择变化。装饰样式与文本输入框保持一致,确保视觉统一。

日期选择器

日期选择用于生产日期和有效期:

Widget _buildDatePicker({
  required String label,
  required DateTime date,
  required ValueChanged<DateTime> onChanged,
}) {
  return GestureDetector(
    onTap: () async {
      final picked = await showDatePicker(
        context: context,
        initialDate: date,
        firstDate: DateTime(2000),
        lastDate: DateTime(2100),
      );
      if (picked != null) {
        onChanged(picked);
      }
    },
    child: Container(
      padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 16.h),
      decoration: BoxDecoration(
        border: Border.all(color: Colors.grey),
        borderRadius: BorderRadius.circular(8.r),
      ),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          Text(
            '$label: ${DateFormat('yyyy-MM-dd').format(date)}',
            style: TextStyle(fontSize: 14.sp),
          ),
          const Icon(Icons.calendar_today, size: 20),
          ],
      ),
    ),
  );
}

使用GestureDetector包裹容器,点击时调用showDatePicker显示系统日期选择器。日期范围设置为2000年到2100年,覆盖了所有可能的药品日期。选择完成后通过onChanged回调更新状态。容器样式模仿输入框,右侧显示日历图标提示用户可以点击。使用DateFormat格式化日期显示为"年-月-日"格式。

分组标题

每个信息组使用标题分隔:

Widget _buildSectionTitle(String title) {
  return Padding(
    padding: EdgeInsets.only(bottom: 12.h),
    child: Text(
      title,
      style: TextStyle(
        fontSize: 16.sp,
        fontWeight: FontWeight.bold,
      ),
    ),
  );
}

标题使用16.sp的加粗字体,比普通文本更醒目。底部添加12.h的间距,与下方的输入字段分隔开。这种分组设计让长表单的结构更加清晰,用户能够快速定位到需要填写的部分。

库存信息布局

数量和单位字段采用横向布局:

Row(
  children: [
    Expanded(
      flex: 2,
      child: _buildTextField(
        controller: _quantityController,
        label: '数量',
        hint: '请输入数量',
        keyboardType: TextInputType.number,
        required: true,
      ),
    ),
    SizedBox(width: 12.w),
    Expanded(
      child: _buildDropdown(
        label: '单位',
        value: _selectedUnit,
        items: _units,
        onChanged: (value) {
          setState(() {
            _selectedUnit = value!;
          });
        },
      ),
    ),
  ],
)

使用Row将数量和单位放在同一行,数量字段占2份空间,单位字段占1份空间,这样的比例让布局更加合理。数量输入框设置keyboardType为数字键盘,方便用户输入。两个字段之间添加12.w的间距,避免紧贴在一起。

保存药品逻辑

表单验证通过后创建药品对象并保存:

void _saveMedicine() {
  if (_formKey.currentState!.validate()) {
    final medicine = Medicine(
      id: const Uuid().v4(),
      name: _nameController.text,
      category: _selectedCategory,
      specification: _specificationController.text,
      manufacturer: _manufacturerController.text,
      productionDate: _productionDate,
      expiryDate: _expiryDate,
      quantity: int.tryParse(_quantityController.text) ?? 0,
      unit: _selectedUnit,
      storageLocation: _selectedLocation,
      usage: _usageController.text,
      dosage: _dosageController.text,
      sideEffects: _sideEffectsController.text,
      contraindications: _contraindicationsController.text,
      barcode: _barcodeController.text.isEmpty ? null : _barcodeController.text,
    );

    context.read<MedicineProvider>().addMedicine(medicine);
    Get.back();
    Get.snackbar('成功', '药品添加成功', snackPosition: SnackPosition.BOTTOM);
  }
}

首先调用_formKey.currentState!.validate()验证所有必填字段。验证通过后,从各个控制器和状态变量中获取数据,创建Medicine对象。使用Uuid().v4()生成唯一ID。数量字段使用int.tryParse转换,如果转换失败默认为0。条形码字段为空时设置为null。

数据保存流程:通过context.read<MedicineProvider>()获取Provider实例,调用addMedicine方法添加药品。Provider会自动通知所有监听者更新UI。使用Get.back()返回上一页,Get.snackbar显示成功提示。这种反馈让用户明确知道操作已完成。

资源释放

在dispose方法中释放所有控制器:


void dispose() {
  _nameController.dispose();
  _specificationController.dispose();
  _manufacturerController.dispose();
  _quantityController.dispose();
  _usageController.dispose();
  _dosageController.dispose();
  _sideEffectsController.dispose();
  _contraindicationsController.dispose();
  _barcodeController.dispose();
  super.dispose();
}

TextEditingController使用完毕后必须释放,否则会造成内存泄漏。在dispose方法中依次调用每个控制器的dispose方法。这是Flutter开发的最佳实践,确保应用的内存使用效率。

表单验证机制

表单验证确保数据的完整性。必填字段通过validator参数进行验证,如果用户未填写,会在字段下方显示红色错误提示。只有所有验证通过,_formKey.currentState!.validate()才会返回true,保存操作才会执行。这种机制有效防止了无效数据的录入。

总结

添加药品页面通过分组表单的设计,让用户能够有条理地录入药品信息。封装的通用组件提高了代码复用性,表单验证确保了数据质量。使用Provider进行状态管理,实现了数据的响应式更新。

条形码扫描功能

支持通过扫描条形码快速录入药品信息:

Widget _buildBarcodeScanner() {
  return GestureDetector(
    onTap: _scanBarcode,
    child: Container(
      padding: EdgeInsets.all(12.w),
      decoration: BoxDecoration(
        color: const Color(0xFF00897B).withOpacity(0.1),
        borderRadius: BorderRadius.circular(8.r),
      ),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Icon(Icons.qr_code_scanner, color: const Color(0xFF00897B), size: 24.sp),
          SizedBox(width: 8.w),
          Text('扫描条形码', style: TextStyle(color: const Color(0xFF00897B), fontSize: 14.sp)),
        ],
      ),
    ),
  );
}

Future<void> _scanBarcode() async {
  final result = await BarcodeScanner.scan();
  if (result.rawContent.isNotEmpty) {
    setState(() {
      _barcodeController.text = result.rawContent;
    });
    // 可以根据条形码查询药品信息
    _lookupMedicineByBarcode(result.rawContent);
  }
}

条形码扫描功能让用户可以快速录入药品信息,避免手动输入的繁琐。扫描后可以自动填充药品名称、规格等信息。

药品图片拍照

支持拍照记录药品外观:

String? _imagePath;

Widget _buildImagePicker() {
  return GestureDetector(
    onTap: _pickImage,
    child: Container(
      width: double.infinity,
      height: 120.h,
      decoration: BoxDecoration(
        color: Colors.grey[100],
        borderRadius: BorderRadius.circular(12.r),
        border: Border.all(color: Colors.grey[300]!),
      ),
      child: _imagePath != null
          ? ClipRRect(
              borderRadius: BorderRadius.circular(12.r),
              child: Image.file(File(_imagePath!), fit: BoxFit.cover),
            )
          : Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Icon(Icons.add_photo_alternate, size: 40.sp, color: Colors.grey[400]),
                SizedBox(height: 8.h),
                Text('添加药品图片', style: TextStyle(color: Colors.grey[500], fontSize: 12.sp)),
              ],
            ),
    ),
  );
}

Future<void> _pickImage() async {
  final picker = ImagePicker();
  final source = await showModalBottomSheet<ImageSource>(
    context: context,
    builder: (context) => Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        ListTile(
          leading: const Icon(Icons.camera_alt),
          title: const Text('拍照'),
          onTap: () => Navigator.pop(context, ImageSource.camera),
        ),
        ListTile(
          leading: const Icon(Icons.photo_library),
          title: const Text('从相册选择'),
          onTap: () => Navigator.pop(context, ImageSource.gallery),
        ),
      ],
    ),
  );
  
  if (source != null) {
    final image = await picker.pickImage(source: source);
    if (image != null) {
      setState(() => _imagePath = image.path);
    }
  }
}

图片功能让用户可以拍照记录药品外观,方便后续识别。支持拍照和从相册选择两种方式。

有效期提醒设置

添加药品时可以设置有效期提醒:

int _expiryWarningDays = 30;

Widget _buildExpiryWarningSelector() {
  return Row(
    children: [
      Expanded(
        child: Text('过期提前提醒', style: TextStyle(fontSize: 14.sp)),
      ),
      DropdownButton<int>(
        value: _expiryWarningDays,
        items: [7, 14, 30, 60, 90].map((days) {
          return DropdownMenuItem(value: days, child: Text('$days天'));
        }).toList(),
        onChanged: (v) => setState(() => _expiryWarningDays = v!),
      ),
    ],
  );
}

有效期提醒设置让用户可以选择提前多少天收到过期提醒。默认30天,用户可以根据药品特性调整。

药品分类建议

根据药品名称自动建议分类:

void _suggestCategory(String name) {
  final categoryKeywords = {
    '感冒用药': ['感冒', '退烧', '发热', '咳嗽'],
    '解热镇痛': ['布洛芬', '阿司匹林', '止痛'],
    '抗生素': ['头孢', '阿莫西林', '青霉素'],
    '消化系统': ['胃', '肠', '消化', '便秘'],
    '心血管': ['降压', '心脏', '血压'],
    '维生素': ['维生素', 'VC', 'VB', '钙片'],
  };
  
  for (final entry in categoryKeywords.entries) {
    for (final keyword in entry.value) {
      if (name.contains(keyword)) {
        setState(() => _selectedCategory = entry.key);
        return;
      }
    }
  }
}

分类建议功能根据药品名称中的关键词自动推荐分类,减少用户的选择操作。


欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐