refactor(item): 重构添加物品页面

- 将 AddItemScreen 中的各个字段提取为独立的 Widget
- 新增 CategoryDropdown、DatePickerField、DescriptionField 等组件
- 优化 Item 模型,使用 ItemIsUse 枚举替代字符串表示是否使用
- 在数据库中添加 price 字段- 重构表单提交逻辑,使用新的组件进行数据采集
This commit is contained in:
LingandRX 2025-05-06 22:06:19 +08:00
parent 083b4e506a
commit faf439087a
11 changed files with 348 additions and 178 deletions

View File

@ -5,6 +5,7 @@ CREATE TABLE items (
category_id INTEGER, category_id INTEGER,
location_id INTEGER, location_id INTEGER,
description TEXT, description TEXT,
price REAL,
purchase_date TEXT, purchase_date TEXT,
is_in_use TEXT DEFAULT 'no', is_in_use TEXT DEFAULT 'no',
status TEXT DEFAULT 'normal', status TEXT DEFAULT 'normal',

View File

@ -1,3 +1,5 @@
import 'package:item_tracker/screens/item_screens/add_item_screen.dart';
class Item { class Item {
int? id; int? id;
// //
@ -8,10 +10,12 @@ class Item {
final int? locationId; final int? locationId;
// //
final String? description; final String? description;
//
final double? price;
// //
final DateTime? purchaseDate; final DateTime? purchaseDate;
// 使 // 使
final String? isInUse; final ItemIsUse? isInUse;
// -normal -deleted // -normal -deleted
final String? status; final String? status;
// //
@ -25,8 +29,9 @@ class Item {
this.categoryId, this.categoryId,
this.locationId, this.locationId,
this.description, this.description,
this.price,
this.purchaseDate, this.purchaseDate,
this.isInUse = 'no', this.isInUse = ItemIsUse.no,
this.status = 'normal', this.status = 'normal',
this.createdAt, this.createdAt,
this.updatedAt, this.updatedAt,
@ -38,8 +43,9 @@ class Item {
'category_id': categoryId, 'category_id': categoryId,
'location_id': locationId, 'location_id': locationId,
'description': description, 'description': description,
'price': price,
'purchase_date': purchaseDate, 'purchase_date': purchaseDate,
'is_in_use': isInUse, 'is_in_use': isInUse?.toInt(),
'status': status, 'status': status,
'created_at': createdAt, 'created_at': createdAt,
'updated_at': updatedAt, 'updated_at': updatedAt,
@ -53,8 +59,11 @@ class Item {
categoryId: map['category_id'], categoryId: map['category_id'],
locationId: map['location_id'], locationId: map['location_id'],
description: map['description'], description: map['description'],
price: map['price'],
purchaseDate: map['purchase_date'], 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'], status: map['status'],
createdAt: map['created_at'], createdAt: map['created_at'],
updatedAt: map['updated_at'], updatedAt: map['updated_at'],

View File

@ -1,5 +1,12 @@
import 'package:flutter/material.dart'; 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:provider/provider.dart';
import 'package:item_tracker/models/item_model.dart'; import 'package:item_tracker/models/item_model.dart';
import 'package:item_tracker/provider/item_provider.dart'; import 'package:item_tracker/provider/item_provider.dart';
@ -11,40 +18,49 @@ enum ItemIsUse {
no, 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 { class AddItemScreen extends StatefulWidget {
@override @override
_FromTestRouteSate createState() => _FromTestRouteSate(); _FromTestRouteSate createState() => _FromTestRouteSate();
} }
class _FromTestRouteSate extends State<AddItemScreen> { class _FromTestRouteSate extends State<AddItemScreen> {
TextEditingController _nameController = TextEditingController(); String _name = '';
TextEditingController _descriptionController = TextEditingController(); String _description = '';
String _location = '';
String _selected = '';
double? _price;
DateTime? _selectedDate;
String? _selectedCategory; // String? _selectedCategory; //
TextEditingController _locationController = TextEditingController();
ItemIsUse? _itemIsUse = ItemIsUse.yes; ItemIsUse? _itemIsUse = ItemIsUse.yes;
TextEditingController _priceController = TextEditingController();
GlobalKey<FormState> _formKey = GlobalKey<FormState>(); GlobalKey<FormState> _formKey = GlobalKey<FormState>();
// //
List<String> _categories = ['A', 'B', 'C', 'D']; // List<String> _categories = ['A', 'B', 'C', 'D']; //
//
void _updateSelectedCategory(String? category) {
setState(() {
_selectedCategory = category;
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var date = selectedDate;
void setGroupValue(ItemIsUse? itemIsUse) {
setState(() {
_itemIsUse = itemIsUse;
});
}
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text('新增物品'), title: Text('新增物品'),
@ -56,174 +72,69 @@ class _FromTestRouteSate extends State<AddItemScreen> {
child: ListView( child: ListView(
padding: EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), padding: EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
children: <Widget>[ children: <Widget>[
TextFormField( NameInputField(initialValue: _name, onChanged: (v) => _name = v),
autofocus: true,
controller: _nameController,
decoration: InputDecoration(
labelText: "名称",
hintText: "请输入物品名称",
border: OutlineInputBorder(),
),
maxLength: 20,
validator: (v) {
if (v == null || v.trim().isEmpty) {
print('名称不能为空');
return "物品名称不能为空";
}
return null;
},
),
SizedBox(height: 16.0), SizedBox(height: 16.0),
TextField( DescriptionField(onChanged: (v) => _description = v),
controller: _descriptionController,
decoration: InputDecoration(
labelText: "物品描述",
hintText: "请输入物品描述",
border: OutlineInputBorder(),
),
maxLines: 4,
maxLength: 200,
),
SizedBox(height: 16.0), SizedBox(height: 16.0),
DropdownButtonFormField<String>( CategoryDropdown(
value: _selectedCategory, categories: _categories,
items: _categories.map((category) { selectedCategory: _selectedCategory,
return DropdownMenuItem<String>( onChanged: (value) {
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),
);
setState(() { 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), SizedBox(height: 16.0),
Wrap( LocationInputField(onChanged: (v) => _location = v),
crossAxisAlignment: WrapCrossAlignment.center,
spacing: 12, //
runSpacing: 8, //
children: [
Text(
'请选择是否使用:',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Radio<ItemIsUse>(
value: ItemIsUse.yes,
groupValue: _itemIsUse,
onChanged: setGroupValue,
),
Text(''),
],
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Radio<ItemIsUse>(
value: ItemIsUse.no,
groupValue: _itemIsUse,
onChanged: setGroupValue,
),
Text(''),
],
),
],
),
SizedBox(height: 16.0), SizedBox(height: 16.0),
TextField( DatePickerField(
controller: _priceController, selectedDate: _selectedDate,
keyboardType: TextInputType.number, onDateSelected: (date) => setState(() {
inputFormatters: [ _selectedDate = date;
FilteringTextInputFormatter.allow( })),
RegExp(r'^\d*\.?\d{0,2}$'), SizedBox(height: 16.0),
), ItemIsUseSelector(
], selected: _itemIsUse,
decoration: InputDecoration( onChanged: (v) => setState(() {
labelText: "物品价格", _itemIsUse = v;
hintText: "请输入物品价格", })),
border: OutlineInputBorder(), SizedBox(height: 16.0),
), PriceInputField(
value: _price,
onChanged: (val) {
setState(() {
_price = val;
});
},
), ),
SizedBox(height: 28.0), SizedBox(height: 28.0),
ElevatedButton( SubmitButton(onPressed: () {
child: Padding( if (_formKey.currentState!.validate()) {
padding: const EdgeInsets.all(16.0), final newItem = Item(
child: Text("提交"), name: _name,
), description: _description,
onPressed: () { purchaseDate: _selectedDate,
if (_formKey.currentState!.validate()) { isInUse: _itemIsUse,
final newItem = Item( price: _price,
name: _nameController.text, );
description: _descriptionController.text,
purchaseDate: selectedDate,
);
print(newItem.toMap()); print(newItem.toMap());
// ItemProvider // ItemProvider
Provider.of<ItemProvider>(context, listen: false) Provider.of<ItemProvider>(context, listen: false)
.addItem(newItem); .addItem(newItem);
// //
ScaffoldMessenger.of(context).showSnackBar(SnackBar( ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('提交成功'), content: Text('提交成功'),
duration: Duration(seconds: 1), duration: Duration(seconds: 1),
)); ));
// Navigator.pop // Navigator.pop
Navigator.pop(context, true); Navigator.pop(context, true);
} }
}, })
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0),
),
elevation: 4.0,
),
),
], ],
), ),
); );

View File

@ -0,0 +1,33 @@
import 'package:flutter/material.dart';
class CategoryDropdown extends StatelessWidget {
final List<String> categories;
final String? selectedCategory;
final ValueChanged<String?> onChanged;
CategoryDropdown({
required this.categories,
required this.selectedCategory,
required this.onChanged,
});
@override
Widget build(BuildContext context) {
return DropdownButtonFormField<String>(
value: selectedCategory,
items: categories.map((category) {
return DropdownMenuItem<String>(
value: category,
child: Text(category),
);
}).toList(),
onChanged: onChanged,
decoration: InputDecoration(
border: OutlineInputBorder(),
filled: true,
fillColor: Colors.grey[100],
labelText: '请选择分类',
),
);
}
}

View File

@ -0,0 +1,45 @@
import 'package:flutter/material.dart';
class DatePickerField extends StatelessWidget {
final DateTime? selectedDate;
final ValueChanged<DateTime?> 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)),
),
),
],
);
}
}

View File

@ -0,0 +1,21 @@
import 'package:flutter/material.dart';
class DescriptionField extends StatelessWidget {
final ValueChanged<String> 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,
);
}
}

View File

@ -0,0 +1,45 @@
import 'package:flutter/material.dart';
import '../add_item_screen.dart';
class ItemIsUseSelector extends StatelessWidget {
final ItemIsUse? selected;
final ValueChanged<ItemIsUse?> 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<ItemIsUse>(
value: ItemIsUse.yes,
groupValue: selected,
onChanged: onChanged),
Text(''),
],
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Radio<ItemIsUse>(
value: ItemIsUse.no,
groupValue: selected,
onChanged: onChanged),
Text(''),
],
),
],
);
}
}

View File

@ -0,0 +1,19 @@
import 'package:flutter/material.dart';
class LocationInputField extends StatelessWidget {
final ValueChanged<String> onChanged;
const LocationInputField({required this.onChanged});
@override
Widget build(BuildContext context) {
return TextFormField(
onChanged: onChanged,
decoration: InputDecoration(
labelText: "物品位置",
hintText: "请输入物品位置",
border: OutlineInputBorder(),
),
);
}
}

View File

@ -0,0 +1,28 @@
import 'package:flutter/material.dart';
class NameInputField extends StatelessWidget {
final String initialValue;
final ValueChanged<String> 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;
},
);
}
}

View File

@ -0,0 +1,37 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class PriceInputField extends StatelessWidget {
final double? value;
final ValueChanged<double?> 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);
},
);
}
}

View File

@ -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,
),
);
}
}