如何用Flutter构建与HTML5动态可增删行表格表单等价的组件
动态行表单Flutter实现(与现有HTML功能完全等价)
核心功能对齐
- 初始状态仅展示1条可编辑数据行
- 支持无限新增数据行,点击任意行的新增按钮时,新行插入到当前操作行的下一个位置
- 仅当表格内剩余数据行总数大于1时,允许删除任意行,最后1行无删除权限
- 新增/删除行后自动按顺序重排行号,保持连续
- 收集的表单数据结构完全匹配现有Go后端的接收格式,无需修改后端接口
具体实现代码
首先定义行数据模型,存储每个输入框的控制器,避免输入状态丢失:
class FormRowData { final TextEditingController purchaseNoController; // 对应原color1字段 final TextEditingController invoiceAmountController; // 对应原color2字段 FormRowData() : purchaseNoController = TextEditingController(), invoiceAmountController = TextEditingController(); void dispose() { purchaseNoController.dispose(); invoiceAmountController.dispose(); } }
主组件的状态逻辑,完全对应原有JS的两个核心操作函数:
class DynamicPurchaseForm extends StatefulWidget { const DynamicPurchaseForm({super.key}); @override State<DynamicPurchaseForm> createState() => _DynamicPurchaseFormState(); } class _DynamicPurchaseFormState extends State<DynamicPurchaseForm> { // 初始化时仅放入1行数据,和原HTML初始状态一致 final List<FormRowData> _rows = [FormRowData()]; @override void dispose() { // 页面销毁时统一释放所有控制器资源 for (var row in _rows) { row.dispose(); } super.dispose(); } // 对应原insRow逻辑:在指定操作行的下一个位置插入新行 void _insertRow(int currentRowIndex) { setState(() { _rows.insert(currentRowIndex + 1, FormRowData()); }); } // 对应原deleteRow逻辑:行数大于1时才允许删除 void _removeRow(int rowIndex) { if (_rows.length > 1) { setState(() { _rows[rowIndex].dispose(); _rows.removeAt(rowIndex); }); } } // 对应原表单提交逻辑:收集所有行的输入值,组装为后端需要的字符串数组 void _submitForm() { final purchaseNoList = _rows.map((e) => e.purchaseNoController.text).toList(); final invoiceAmountList = _rows.map((e) => e.invoiceAmountController.text).toList(); // 直接将两个列表按原接口格式序列化发送,和现有Go后端接收的Color1、Color2字段完全匹配 } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('采购申请金额分摊')), body: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // 表格标题,对应原HTML的legend文本 const Text( 'توزيع مبلغ البيان على طلبات الشراء', textAlign: TextAlign.center, style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ), const SizedBox(height: 12), // 表格布局 Table( border: TableBorder.all(color: Colors.grey.shade300), columnWidths: const { 0: FixedColumnWidth(60), // 序号列 1: FlexColumnWidth(), // 采购申请号列 2: FlexColumnWidth(), // 发票金额列 3: FixedColumnWidth(56), // 新增按钮列 4: FixedColumnWidth(56), // 删除按钮列 }, children: [ // 表头行,和原HTML thead内容一一对应 const TableRow( decoration: BoxDecoration(color: Color(0xFFf5f5f5)), children: [ Padding( padding: EdgeInsets.all(8), child: Text('التسلسل', textAlign: TextAlign.center), ), Padding( padding: EdgeInsets.all(8), child: Text('رقم طلب الشراء', textAlign: TextAlign.center), ), Padding( padding: EdgeInsets.all(8), child: Text('قيمة فاتورة طلب الشراء', textAlign: TextAlign.center), ), Padding( padding: EdgeInsets.all(8), child: Text('إضافه/حذف', textAlign: TextAlign.center), ), SizedBox.shrink() ], ), // 动态生成所有数据行 ..._rows.asMap().entries.map((entry) { int rowIndex = entry.key; FormRowData rowData = entry.value; return TableRow( children: [ // 自动生成行号:遍历索引+1,无需手动更新 Padding( padding: const EdgeInsets.all(8), child: Text( '${rowIndex + 1}', textAlign: TextAlign.center, ), ), // 采购申请号输入框,对应原color1输入 Padding( padding: const EdgeInsets.all(8), child: TextField( controller: rowData.purchaseNoController, decoration: const InputDecoration( border: OutlineInputBorder(), isDense: true, ), ), ), // 发票金额输入框,对应原color2输入 Padding( padding: const EdgeInsets.all(8), child: TextField( controller: rowData.invoiceAmountController, decoration: const InputDecoration( border: OutlineInputBorder(), isDense: true, ), ), ), // 新增按钮 Center( child: IconButton( onPressed: () => _insertRow(rowIndex), icon: const Icon(Icons.add_circle_outline), ), ), // 删除按钮:仅当行数大于1时可点击 Center( child: IconButton( onPressed: _rows.length > 1 ? () => _removeRow(rowIndex) : null, icon: const Icon(Icons.remove_circle_outline), color: Colors.red, ), ), ], ); }) ], ), const SizedBox(height: 20), // 提交按钮,对应原HTML的submit按钮 ElevatedButton( onPressed: _submitForm, style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 12), ), child: const Text('SEND إرسال'), ) ], ), ), ); } }
实现细节说明
- 序号自动维护:行号是通过遍历行集合的实时索引计算生成的,每次插入、删除行触发状态更新后,序号会自动保持连续,完全替代原JS中循环更新序号的手动逻辑
- 删除权限校验:按钮的点击状态直接和
_rows.length > 1的判断绑定,和原JS中的删除前置校验完全一致,从交互上避免删除最后一行 - 插入位置对齐:新行默认插入到点击按钮所在行的下一个位置,和原JS中
insertBefore的插入逻辑一致,不会出现新行统一追加到表格末尾的差异 - 后端兼容:提交时收集的两个字符串数组结构,和现有Go代码中
Color1 []string、Color2 []string的字段类型完全匹配,不需要调整后端的参数解析逻辑 - 内存安全:删除行、页面销毁时会主动调用控制器的
dispose方法释放资源,避免内存泄漏
内容的提问来源于stack exchange,提问作者Hasan A Yousef
相关产品推荐
相关产品推荐

