From faf439087afb8d2bbf169336981a330b49f6e2ae Mon Sep 17 00:00:00 2001 From: LingandRX Date: Tue, 6 May 2025 22:06:19 +0800 Subject: [PATCH] =?UTF-8?q?refactor(item):=20=E9=87=8D=E6=9E=84=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E7=89=A9=E5=93=81=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 AddItemScreen 中的各个字段提取为独立的 Widget - 新增 CategoryDropdown、DatePickerField、DescriptionField 等组件 - 优化 Item 模型,使用 ItemIsUse 枚举替代字符串表示是否使用 - 在数据库中添加 price 字段- 重构表单提交逻辑,使用新的组件进行数据采集 --- lib/database/item_table.dart | 1 + lib/models/item_model.dart | 17 +- lib/screens/item_screens/add_item_screen.dart | 259 ++++++------------ .../widgets/category_dropdown.dart | 33 +++ .../widgets/date_picker_field.dart | 45 +++ .../widgets/description_field.dart | 21 ++ .../widgets/item_is_use_selector.dart | 45 +++ .../widgets/location_input_field.dart | 19 ++ .../widgets/name_input_field.dart | 28 ++ .../widgets/price_input_field.dart | 37 +++ .../item_screens/widgets/submit_button.dart | 21 ++ 11 files changed, 348 insertions(+), 178 deletions(-) create mode 100644 lib/screens/item_screens/widgets/category_dropdown.dart create mode 100644 lib/screens/item_screens/widgets/date_picker_field.dart create mode 100644 lib/screens/item_screens/widgets/description_field.dart create mode 100644 lib/screens/item_screens/widgets/item_is_use_selector.dart create mode 100644 lib/screens/item_screens/widgets/location_input_field.dart create mode 100644 lib/screens/item_screens/widgets/name_input_field.dart create mode 100644 lib/screens/item_screens/widgets/price_input_field.dart create mode 100644 lib/screens/item_screens/widgets/submit_button.dart diff --git a/lib/database/item_table.dart b/lib/database/item_table.dart index ddadb7e..e967e98 100644 --- a/lib/database/item_table.dart +++ b/lib/database/item_table.dart @@ -5,6 +5,7 @@ CREATE TABLE items ( category_id INTEGER, location_id INTEGER, description TEXT, + price REAL, purchase_date TEXT, is_in_use TEXT DEFAULT 'no', status TEXT DEFAULT 'normal', diff --git a/lib/models/item_model.dart b/lib/models/item_model.dart index 659ecfb..fb1cbe5 100644 --- a/lib/models/item_model.dart +++ b/lib/models/item_model.dart @@ -1,3 +1,5 @@ +import 'package:item_tracker/screens/item_screens/add_item_screen.dart'; + class Item { int? id; // 名称 @@ -8,10 +10,12 @@ class Item { final int? locationId; // 描述 final String? description; + // 价格 + final double? price; // 购买日期 final DateTime? purchaseDate; // 是否使用 - final String? isInUse; + final ItemIsUse? isInUse; // 数据状态 -normal -deleted final String? status; // 创建时间 @@ -25,8 +29,9 @@ class Item { this.categoryId, this.locationId, this.description, + this.price, this.purchaseDate, - this.isInUse = 'no', + this.isInUse = ItemIsUse.no, this.status = 'normal', this.createdAt, this.updatedAt, @@ -38,8 +43,9 @@ class Item { 'category_id': categoryId, 'location_id': locationId, 'description': description, + 'price': price, 'purchase_date': purchaseDate, - 'is_in_use': isInUse, + 'is_in_use': isInUse?.toInt(), 'status': status, 'created_at': createdAt, 'updated_at': updatedAt, @@ -53,8 +59,11 @@ class Item { categoryId: map['category_id'], locationId: map['location_id'], description: map['description'], + price: map['price'], purchaseDate: map['purchase_date'], - isInUse: map['is_in_use'], + isInUse: map['is_in_use'] != null + ? ItemIsUseX.fromInt(int.parse(map['is_in_use'].toString())) + : ItemIsUse.no, status: map['status'], createdAt: map['created_at'], updatedAt: map['updated_at'], diff --git a/lib/screens/item_screens/add_item_screen.dart b/lib/screens/item_screens/add_item_screen.dart index 88f1dde..36795b2 100644 --- a/lib/screens/item_screens/add_item_screen.dart +++ b/lib/screens/item_screens/add_item_screen.dart @@ -1,5 +1,12 @@ import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; +import 'package:item_tracker/screens/item_screens/widgets/category_dropdown.dart'; +import 'package:item_tracker/screens/item_screens/widgets/date_picker_field.dart'; +import 'package:item_tracker/screens/item_screens/widgets/description_field.dart'; +import 'package:item_tracker/screens/item_screens/widgets/item_is_use_selector.dart'; +import 'package:item_tracker/screens/item_screens/widgets/location_input_field.dart'; +import 'package:item_tracker/screens/item_screens/widgets/name_input_field.dart'; +import 'package:item_tracker/screens/item_screens/widgets/price_input_field.dart'; +import 'package:item_tracker/screens/item_screens/widgets/submit_button.dart'; import 'package:provider/provider.dart'; import 'package:item_tracker/models/item_model.dart'; import 'package:item_tracker/provider/item_provider.dart'; @@ -11,40 +18,49 @@ enum ItemIsUse { no, } +extension ItemIsUseX on ItemIsUse { + int toInt() { + switch (this) { + case ItemIsUse.yes: + return 1; + case ItemIsUse.no: + return 0; + } + } + + static ItemIsUse? fromInt(int? value) { + switch (value) { + case 1: + return ItemIsUse.yes; + case 0: + return ItemIsUse.no; + default: + return null; + } + } +} + class AddItemScreen extends StatefulWidget { @override _FromTestRouteSate createState() => _FromTestRouteSate(); } class _FromTestRouteSate extends State { - TextEditingController _nameController = TextEditingController(); - TextEditingController _descriptionController = TextEditingController(); + String _name = ''; + String _description = ''; + String _location = ''; + String _selected = ''; + double? _price; + DateTime? _selectedDate; String? _selectedCategory; // 当前选中的分类 - TextEditingController _locationController = TextEditingController(); ItemIsUse? _itemIsUse = ItemIsUse.yes; - TextEditingController _priceController = TextEditingController(); GlobalKey _formKey = GlobalKey(); // 添加自定义分类列表 List _categories = ['A', 'B', 'C', 'D']; // 自定义分类 - // 更新选中的分类 - void _updateSelectedCategory(String? category) { - setState(() { - _selectedCategory = category; - }); - } - @override Widget build(BuildContext context) { - var date = selectedDate; - - void setGroupValue(ItemIsUse? itemIsUse) { - setState(() { - _itemIsUse = itemIsUse; - }); - } - return Scaffold( appBar: AppBar( title: Text('新增物品'), @@ -56,174 +72,69 @@ class _FromTestRouteSate extends State { child: ListView( padding: EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), children: [ - TextFormField( - autofocus: true, - controller: _nameController, - decoration: InputDecoration( - labelText: "名称", - hintText: "请输入物品名称", - border: OutlineInputBorder(), - ), - maxLength: 20, - validator: (v) { - if (v == null || v.trim().isEmpty) { - print('名称不能为空'); - return "物品名称不能为空"; - } - return null; - }, - ), + NameInputField(initialValue: _name, onChanged: (v) => _name = v), SizedBox(height: 16.0), - TextField( - controller: _descriptionController, - decoration: InputDecoration( - labelText: "物品描述", - hintText: "请输入物品描述", - border: OutlineInputBorder(), - ), - maxLines: 4, - maxLength: 200, - ), + DescriptionField(onChanged: (v) => _description = v), SizedBox(height: 16.0), - DropdownButtonFormField( - value: _selectedCategory, - items: _categories.map((category) { - return DropdownMenuItem( - value: category, - child: Text(category), - ); - }).toList(), - onChanged: _updateSelectedCategory, - decoration: InputDecoration( - border: OutlineInputBorder(), - filled: true, - fillColor: Colors.grey[100], - labelText: '请选择分类', - ), - ), - SizedBox(height: 16.0), - TextFormField( - controller: _locationController, - decoration: InputDecoration( - labelText: "物品位置", - hintText: "请输入物品位置", - border: OutlineInputBorder(), - ), - ), - SizedBox(height: 16.0), - Text( - date == null - ? '请选择日期' - : '已选择日期: ${date.year}年${date.month}月${date.day}日', - ), - SizedBox(height: 16.0), - ElevatedButton.icon( - icon: Icon(Icons.calendar_today, color: Colors.white), - onPressed: () async { - var pickedDate = await showDatePicker( - context: context, - initialEntryMode: DatePickerEntryMode.calendarOnly, - initialDate: DateTime.now(), - firstDate: DateTime(2015, 8), - lastDate: DateTime(2101), - ); + CategoryDropdown( + categories: _categories, + selectedCategory: _selectedCategory, + onChanged: (value) { setState(() { - selectedDate = pickedDate; + _selectedCategory = value; }); }, - label: Text('选择日期', style: TextStyle(color: Colors.white)), - style: ElevatedButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), - ), - ), ), SizedBox(height: 16.0), - Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - spacing: 12, // 每个元素之间水平间距 - runSpacing: 8, // 换行后垂直间距 - children: [ - Text( - '请选择是否使用:', - style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), - ), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Radio( - value: ItemIsUse.yes, - groupValue: _itemIsUse, - onChanged: setGroupValue, - ), - Text('是'), - ], - ), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Radio( - value: ItemIsUse.no, - groupValue: _itemIsUse, - onChanged: setGroupValue, - ), - Text('否'), - ], - ), - ], - ), + LocationInputField(onChanged: (v) => _location = v), SizedBox(height: 16.0), - TextField( - controller: _priceController, - keyboardType: TextInputType.number, - inputFormatters: [ - FilteringTextInputFormatter.allow( - RegExp(r'^\d*\.?\d{0,2}$'), - ), - ], - decoration: InputDecoration( - labelText: "物品价格", - hintText: "请输入物品价格", - border: OutlineInputBorder(), - ), + DatePickerField( + selectedDate: _selectedDate, + onDateSelected: (date) => setState(() { + _selectedDate = date; + })), + SizedBox(height: 16.0), + ItemIsUseSelector( + selected: _itemIsUse, + onChanged: (v) => setState(() { + _itemIsUse = v; + })), + SizedBox(height: 16.0), + PriceInputField( + value: _price, + onChanged: (val) { + setState(() { + _price = val; + }); + }, ), SizedBox(height: 28.0), - ElevatedButton( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Text("提交"), - ), - onPressed: () { - if (_formKey.currentState!.validate()) { - final newItem = Item( - name: _nameController.text, - description: _descriptionController.text, - purchaseDate: selectedDate, - ); + SubmitButton(onPressed: () { + if (_formKey.currentState!.validate()) { + final newItem = Item( + name: _name, + description: _description, + purchaseDate: _selectedDate, + isInUse: _itemIsUse, + price: _price, + ); - print(newItem.toMap()); + print(newItem.toMap()); - // 获取 ItemProvider 并添加物品 - Provider.of(context, listen: false) - .addItem(newItem); + // 获取 ItemProvider 并添加物品 + Provider.of(context, listen: false) + .addItem(newItem); - // 弹窗提示添加成功 - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text('提交成功'), - duration: Duration(seconds: 1), - )); + // 弹窗提示添加成功 + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text('提交成功'), + duration: Duration(seconds: 1), + )); - // 确保只有在表单验证成功后才调用 Navigator.pop - Navigator.pop(context, true); - } - }, - style: ElevatedButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), - ), - elevation: 4.0, - ), - ), + // 确保只有在表单验证成功后才调用 Navigator.pop + Navigator.pop(context, true); + } + }) ], ), ); diff --git a/lib/screens/item_screens/widgets/category_dropdown.dart b/lib/screens/item_screens/widgets/category_dropdown.dart new file mode 100644 index 0000000..9cd6a09 --- /dev/null +++ b/lib/screens/item_screens/widgets/category_dropdown.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; + +class CategoryDropdown extends StatelessWidget { + final List categories; + final String? selectedCategory; + final ValueChanged onChanged; + + CategoryDropdown({ + required this.categories, + required this.selectedCategory, + required this.onChanged, + }); + + @override + Widget build(BuildContext context) { + return DropdownButtonFormField( + value: selectedCategory, + items: categories.map((category) { + return DropdownMenuItem( + value: category, + child: Text(category), + ); + }).toList(), + onChanged: onChanged, + decoration: InputDecoration( + border: OutlineInputBorder(), + filled: true, + fillColor: Colors.grey[100], + labelText: '请选择分类', + ), + ); + } +} diff --git a/lib/screens/item_screens/widgets/date_picker_field.dart b/lib/screens/item_screens/widgets/date_picker_field.dart new file mode 100644 index 0000000..fec7750 --- /dev/null +++ b/lib/screens/item_screens/widgets/date_picker_field.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; + +class DatePickerField extends StatelessWidget { + final DateTime? selectedDate; + final ValueChanged onDateSelected; + const DatePickerField({ + required this.selectedDate, + required this.onDateSelected, + }); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + selectedDate == null + ? '请选择日期' + : '已选择日期: ${selectedDate!.year}年${selectedDate!.month}月${selectedDate!.day}日', + ), + SizedBox(height: 8), + ElevatedButton.icon( + icon: Icon(Icons.calendar_today, color: Colors.white), + label: Text('选择日期'), + onPressed: () async { + final pickedDate = await showDatePicker( + context: context, + initialEntryMode: DatePickerEntryMode.calendarOnly, + initialDate: DateTime.now(), + firstDate: DateTime(2015, 8), + lastDate: DateTime(2101), + ); + if (pickedDate == null) { + return; + } + onDateSelected(pickedDate); + }, + style: ElevatedButton.styleFrom( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ), + ), + ], + ); + } +} diff --git a/lib/screens/item_screens/widgets/description_field.dart b/lib/screens/item_screens/widgets/description_field.dart new file mode 100644 index 0000000..9fbdd46 --- /dev/null +++ b/lib/screens/item_screens/widgets/description_field.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; + +class DescriptionField extends StatelessWidget { + final ValueChanged onChanged; + + const DescriptionField({required this.onChanged}); + + @override + Widget build(BuildContext context) { + return TextField( + decoration: InputDecoration( + labelText: "物品描述", + hintText: "请输入物品描述", + border: OutlineInputBorder(), + ), + maxLines: 4, + maxLength: 200, + onChanged: onChanged, + ); + } +} diff --git a/lib/screens/item_screens/widgets/item_is_use_selector.dart b/lib/screens/item_screens/widgets/item_is_use_selector.dart new file mode 100644 index 0000000..97b697e --- /dev/null +++ b/lib/screens/item_screens/widgets/item_is_use_selector.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; + +import '../add_item_screen.dart'; + +class ItemIsUseSelector extends StatelessWidget { + final ItemIsUse? selected; + final ValueChanged onChanged; + + const ItemIsUseSelector({required this.selected, required this.onChanged}); + + @override + Widget build(BuildContext context) { + return Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + spacing: 12, + runSpacing: 8, + children: [ + Text( + '请选择是否使用:', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Radio( + value: ItemIsUse.yes, + groupValue: selected, + onChanged: onChanged), + Text('是'), + ], + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Radio( + value: ItemIsUse.no, + groupValue: selected, + onChanged: onChanged), + Text('否'), + ], + ), + ], + ); + } +} diff --git a/lib/screens/item_screens/widgets/location_input_field.dart b/lib/screens/item_screens/widgets/location_input_field.dart new file mode 100644 index 0000000..80aaf52 --- /dev/null +++ b/lib/screens/item_screens/widgets/location_input_field.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; + +class LocationInputField extends StatelessWidget { + final ValueChanged onChanged; + + const LocationInputField({required this.onChanged}); + + @override + Widget build(BuildContext context) { + return TextFormField( + onChanged: onChanged, + decoration: InputDecoration( + labelText: "物品位置", + hintText: "请输入物品位置", + border: OutlineInputBorder(), + ), + ); + } +} diff --git a/lib/screens/item_screens/widgets/name_input_field.dart b/lib/screens/item_screens/widgets/name_input_field.dart new file mode 100644 index 0000000..62bdad4 --- /dev/null +++ b/lib/screens/item_screens/widgets/name_input_field.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; + +class NameInputField extends StatelessWidget { + final String initialValue; + final ValueChanged onChanged; + + const NameInputField({required this.initialValue, required this.onChanged}); + + @override + Widget build(BuildContext context) { + return TextFormField( + initialValue: initialValue, + onChanged: onChanged, + decoration: InputDecoration( + labelText: '名称', + hintText: '请输入名称', + border: OutlineInputBorder(), + ), + maxLength: 20, + validator: (value) { + if (value == null || value.trim().isEmpty) { + return '名称不能为空'; + } + return null; + }, + ); + } +} diff --git a/lib/screens/item_screens/widgets/price_input_field.dart b/lib/screens/item_screens/widgets/price_input_field.dart new file mode 100644 index 0000000..220f728 --- /dev/null +++ b/lib/screens/item_screens/widgets/price_input_field.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class PriceInputField extends StatelessWidget { + final double? value; + final ValueChanged onChanged; + + const PriceInputField({ + Key? key, + required this.value, + required this.onChanged, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final controller = TextEditingController( + text: value != null ? value.toString() : '', + ); + + return TextField( + controller: controller, + keyboardType: TextInputType.numberWithOptions(decimal: true), + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}$')), + ], + decoration: InputDecoration( + labelText: "物品价格", + hintText: "请输入物品价格", + border: OutlineInputBorder(), + ), + onChanged: (text) { + final parsed = double.tryParse(text); + onChanged(parsed); + }, + ); + } +} diff --git a/lib/screens/item_screens/widgets/submit_button.dart b/lib/screens/item_screens/widgets/submit_button.dart new file mode 100644 index 0000000..b4611fb --- /dev/null +++ b/lib/screens/item_screens/widgets/submit_button.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; + +class SubmitButton extends StatelessWidget { + final VoidCallback onPressed; + const SubmitButton({required this.onPressed}); + + @override + Widget build(BuildContext context) { + return ElevatedButton( + onPressed: onPressed, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Text('提交'), + ), + style: ElevatedButton.styleFrom( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + elevation: 4, + ), + ); + } +}