You need to enable JavaScript to run this app. 如何用Flutter构建与HTML5动态可增删行表格表单等价的组件
优惠活动
大模型
产品
解决方案
定价
更多

如何用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.30 22:15:01