在这里插入图片描述
编辑药品功能让用户可以修改已录入的药品信息。当药品信息发生变化时,比如库存调整、有效期更新等,用户需要通过编辑功能来保持数据的准确性。

功能设计要点

编辑页面与添加页面类似,但需要预填充现有数据。用户可以修改任何字段,保存后更新到Provider中。页面需要处理数据初始化、表单验证和数据更新三个核心流程。

数据初始化

在initState中初始化所有控制器和状态:


void initState() {
  super.initState();
  _nameController = TextEditingController(text: widget.medicine.name);
  _specificationController = TextEditingController(text: widget.medicine.specification);
  _manufacturerController = TextEditingController(text: widget.medicine.manufacturer);
  _quantityController = TextEditingController(text: widget.medicine.quantity.toString());
  _usageController = TextEditingController(text: widget.medicine.usage);
  _dosageController = TextEditingController(text: widget.medicine.dosage);
  _sideEffectsController = TextEditingController(text: widget.medicine.sideEffects);
  _contraindicationsController = TextEditingController(text: widget.medicine.contraindications);

  _selectedCategory = _categories.contains(widget.medicine.category)
      ? widget.medicine.category
      : '其他';
  _selectedUnit = _units.contains(widget.medicine.unit)
      ? widget.medicine.unit
      : '粒';
  _selectedLocation = _locations.contains(widget.medicine.storageLocation)
      ? widget.medicine.storageLocation
      : '其他';
  _productionDate = widget.medicine.productionDate;
  _expiryDate = widget.medicine.expiryDate;
}

每个TextEditingController在创建时传入初始值,这样输入框会显示现有数据。对于下拉选择字段,需要检查值是否在选项列表中,如果不在则使用默认值。这种处理避免了下拉框因为值不存在而报错。日期字段直接使用药品对象中的DateTime值。

页面结构

编辑页面的结构与添加页面基本一致:


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: [
            _buildTextField(_nameController, '药品名称', required: true),
            _buildDropdown('药品分类', _selectedCategory, _categories, (v) {
              setState(() => _selectedCategory = v!);
            }),
            _buildTextField(_specificationController, '规格', required: true),
            // 更多字段...
            SizedBox(
              width: double.infinity,
              height: 48.h,
              child: ElevatedButton(
                onPressed: _updateMedicine,
                child: Text('保存修改'),
              ),
            ),
          ],
        ),
      ),
    ),
  );
}

使用Form组件管理表单验证,SingleChildScrollView支持滚动。所有输入字段垂直排列,底部是保存按钮。按钮文字改为"保存修改",明确告知用户这是编辑操作而非新增。

文本输入组件

封装的文本输入组件支持多种配置:

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

这个组件与添加页面的完全相同,实现了代码复用。通过参数控制是否必填、键盘类型和最大行数。必填字段会在标签后添加星号,并在验证时检查是否为空。

下拉选择组件

下拉选择用于固定选项的字段:

Widget _buildDropdown(
  String label,
  String value,
  List<String> items,
  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((i) => DropdownMenuItem(value: i, child: Text(i))).toList(),
    onChanged: onChanged,
  );
}

下拉框显示当前选中的值,用户点击后可以从列表中选择新值。选择变化时通过onChanged回调更新状态。装饰样式与文本输入框保持一致,确保视觉统一。

日期选择器

日期选择器调用系统日期选择对话框:

Widget _buildDatePicker(String label, DateTime date, 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)}'),
          const Icon(Icons.calendar_today, size: 20),
        ],
      ),
    ),
  );
}

容器显示当前日期,点击时弹出日期选择器。initialDate设置为当前日期,用户可以在此基础上调整。选择完成后通过回调更新状态,界面自动刷新显示新日期。

数量和单位布局

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

Row(
  children: [
    Expanded(
      flex: 2,
      child: _buildTextField(
        _quantityController,
        '数量',
        keyboardType: TextInputType.number,
        required: true,
      ),
    ),
    SizedBox(width: 12.w),
    Expanded(
      child: _buildDropdown('单位', _selectedUnit, _units, (v) {
        setState(() => _selectedUnit = v!);
      }),
    ),
  ],
)

数量字段占2份空间,单位字段占1份空间。数量输入框使用数字键盘,方便用户输入。两个字段之间添加间距,避免视觉上的拥挤。

更新药品逻辑

保存时使用copyWith方法创建新对象:

void _updateMedicine() {
  if (_formKey.currentState!.validate()) {
    final updated = widget.medicine.copyWith(
      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,
    );

    context.read<MedicineProvider>().updateMedicine(updated);
    Get.back();
    Get.snackbar('成功', '药品信息已更新', snackPosition: SnackPosition.BOTTOM);
  }
}

首先验证表单,确保必填字段都已填写。使用copyWith方法基于原对象创建新对象,只更新修改的字段,保持ID等不变。调用Provider的updateMedicine方法更新数据,Provider会通知所有监听者刷新UI。返回上一页并显示成功提示,让用户知道操作已完成。

copyWith方法的优势

Medicine模型中的copyWith方法让更新操作更加安全:

Medicine copyWith({
  String? name,
  String? category,
  // 其他字段...
}) {
  return Medicine(
    id: id ?? this.id,
    name: name ?? this.name,
    category: category ?? this.category,
    // 其他字段...
  );
}

这个方法创建一个新的Medicine对象,只替换传入的字段,其他字段保持不变。这种不可变数据的设计模式让状态管理更加可靠,避免了直接修改对象可能带来的问题。

资源管理

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


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

每个TextEditingController都需要手动释放,这是Flutter的内存管理要求。忘记释放会导致内存泄漏,影响应用性能。

编辑与添加的区别

编辑页面与添加页面的主要区别在于数据初始化。编辑页面需要在initState中预填充数据,而添加页面使用空值或默认值。保存逻辑也不同:编辑使用updateMedicine方法,添加使用addMedicine方法。通过这种设计,两个页面可以共享大部分UI组件代码。

表单验证增强

为编辑表单添加更完善的验证逻辑:

String? _validateQuantity(String? value) {
  if (value == null || value.isEmpty) {
    return '请输入数量';
  }
  final quantity = int.tryParse(value);
  if (quantity == null) {
    return '请输入有效的数字';
  }
  if (quantity < 0) {
    return '数量不能为负数';
  }
  if (quantity > 9999) {
    return '数量不能超过9999';
  }
  return null;
}

String? _validateExpiryDate() {
  if (_expiryDate.isBefore(_productionDate)) {
    return '有效期不能早于生产日期';
  }
  return null;
}

数量验证检查是否为空、是否为有效数字、是否在合理范围内。日期验证确保有效期不早于生产日期。这些验证在用户提交前进行,避免保存无效数据。

变更检测功能

检测用户是否修改了数据,在返回时提示保存:

bool _hasChanges() {
  return _nameController.text != widget.medicine.name ||
      _selectedCategory != widget.medicine.category ||
      _specificationController.text != widget.medicine.specification ||
      _manufacturerController.text != widget.medicine.manufacturer ||
      _productionDate != widget.medicine.productionDate ||
      _expiryDate != widget.medicine.expiryDate ||
      int.tryParse(_quantityController.text) != widget.medicine.quantity ||
      _selectedUnit != widget.medicine.unit ||
      _selectedLocation != widget.medicine.storageLocation;
}

Future<bool> _onWillPop() async {
  if (!_hasChanges()) return true;
  
  final result = await showDialog<bool>(
    context: context,
    builder: (context) => AlertDialog(
      title: const Text('放弃修改?'),
      content: const Text('您有未保存的修改,确定要放弃吗?'),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(context, false),
          child: const Text('继续编辑'),
        ),
        TextButton(
          onPressed: () => Navigator.pop(context, true),
          child: const Text('放弃'),
        ),
      ],
    ),
  );
  return result ?? false;
}

_hasChanges方法比较当前值与原始值,判断是否有修改。使用WillPopScope包裹页面,在用户返回时检测变更。如果有未保存的修改,弹出确认对话框,防止用户意外丢失数据。

快速调整数量

为数量字段添加快速调整按钮:

Widget _buildQuantityField() {
  return Row(
    children: [
      IconButton(
        icon: const Icon(Icons.remove_circle_outline),
        onPressed: () {
          final current = int.tryParse(_quantityController.text) ?? 0;
          if (current > 0) {
            _quantityController.text = (current - 1).toString();
          }
        },
      ),
      Expanded(
        child: TextFormField(
          controller: _quantityController,
          keyboardType: TextInputType.number,
          textAlign: TextAlign.center,
          decoration: InputDecoration(
            border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.r)),
          ),
          validator: _validateQuantity,
        ),
      ),
      IconButton(
        icon: const Icon(Icons.add_circle_outline),
        onPressed: () {
          final current = int.tryParse(_quantityController.text) ?? 0;
          _quantityController.text = (current + 1).toString();
        },
      ),
    ],
  );
}

数量字段两侧添加加减按钮,用户可以快速调整数量而无需手动输入。这种设计特别适合库存盘点场景,提升了操作效率。

图片编辑功能

支持编辑药品图片:

String? _imagePath;

Widget _buildImagePicker() {
  return GestureDetector(
    onTap: _pickImage,
    child: Container(
      width: double.infinity,
      height: 150.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: 48.sp, color: Colors.grey[400]),
                SizedBox(height: 8.h),
                Text('点击添加药品图片', style: TextStyle(color: Colors.grey[500])),
              ],
            ),
    ),
  );
}

Future<void> _pickImage() async {
  final picker = ImagePicker();
  final image = await picker.pickImage(source: ImageSource.gallery);
  if (image != null) {
    setState(() => _imagePath = image.path);
  }
}

图片选择器显示当前图片或占位提示。点击后打开相册选择新图片。图片使用圆角裁剪,与卡片风格保持一致。

删除药品功能

在编辑页面提供删除药品的入口:

Widget _buildDeleteButton() {
  return TextButton(
    onPressed: _showDeleteConfirmation,
    child: const Text('删除此药品', style: TextStyle(color: Colors.red)),
  );
}

void _showDeleteConfirmation() {
  showDialog(
    context: context,
    builder: (context) => AlertDialog(
      title: const Text('确认删除'),
      content: Text('确定要删除"${widget.medicine.name}"吗?此操作无法撤销。'),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(context),
          child: const Text('取消'),
        ),
        TextButton(
          onPressed: () {
            Navigator.pop(context);
            context.read<MedicineProvider>().deleteMedicine(widget.medicine.id);
            Get.back();
            Get.snackbar('已删除', '药品已删除', snackPosition: SnackPosition.BOTTOM);
          },
          child: const Text('删除', style: TextStyle(color: Colors.red)),
        ),
      ],
    ),
  );
}

删除按钮使用红色文字,放在页面底部。点击后弹出确认对话框,说明操作不可撤销。确认删除后返回上一页并显示提示。

历史记录功能

记录药品的修改历史:

class MedicineEditHistory {
  final String medicineId;
  final DateTime editTime;
  final String editType;
  final Map<String, dynamic> changes;

  MedicineEditHistory({
    required this.medicineId,
    required this.editTime,
    required this.editType,
    required this.changes,
  });
}

void _saveEditHistory(Medicine original, Medicine updated) {
  final changes = <String, dynamic>{};
  
  if (original.name != updated.name) {
    changes['name'] = {'from': original.name, 'to': updated.name};
  }
  if (original.quantity != updated.quantity) {
    changes['quantity'] = {'from': original.quantity, 'to': updated.quantity};
  }
  // 记录其他字段变更...
  
  if (changes.isNotEmpty) {
    final history = MedicineEditHistory(
      medicineId: original.id,
      editTime: DateTime.now(),
      editType: 'update',
      changes: changes,
    );
    // 保存历史记录
  }
}

修改历史记录每次编辑的时间和变更内容。用户可以查看药品的修改历史,了解数据的变化过程。这对于追踪库存变化特别有用。

总结

编辑药品页面通过预填充数据和copyWith方法,实现了安全可靠的数据更新功能。表单验证确保数据完整性,Provider管理状态更新,用户体验流畅自然。


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

Logo

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

更多推荐