You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Dash应用中实现输入表单的多行重复功能?

多行输入表单实现方案(Dash)

问题核心

要实现可动态添加行的支出输入表单,每个输入组件确实需要唯一ID,但不用手动为每行写重复代码,可以通过模式匹配回调+动态ID规则来简化开发,避免冗余。

实现思路

  • 用dcc.Store存储当前行数,作为动态生成组件的索引依据
  • 为每行的同类组件设置统一的ID格式(如{'type': 'amount-spent', 'index': 行号}),让模式匹配回调能批量处理所有行
  • 编写通用回调,自动处理所有行的Total计算、提交按钮状态更新
  • 实现添加/删除行逻辑,更新行数并渲染对应的输入行

修改后完整代码

from dash import Dash, dcc, html, dash_table, Input, Output, State, callback, MATCH, ALL
import dash_bootstrap_components as dbc
import pandas as pd

app = Dash(
    __name__,
    external_stylesheets=[dbc.themes.SPACELAB, dbc.icons.FONT_AWESOME],
)

app.layout = dbc.Row(dbc.Col(dbc.Tabs([
    dbc.Tab(html.H2("Dummy Tab"), label="Dummy"),
    dbc.Tab(dbc.Container([
        # 表头
        dbc.Row([
            dbc.Col(html.H4("Category"), width=12, lg=2),
            dbc.Col(html.H4("Amount Spent"), width=12, lg=3),
            dbc.Col(html.H4("Tax Rate"), width=12, lg=3),
            dbc.Col(html.H4("Total"), width=12, lg=3),
            dbc.Col(width=12, lg=1)
        ], className="mb-3"),
        
        # 动态输入行容器
        html.Div(id="input-rows-container"),
        
        # 控制按钮
        dbc.Row([
            dbc.Col(html.Button("添加行", id="add-row-btn", n_clicks=0), width="auto"),
            dbc.Col(html.Button("提交所有", id="submit-all-btn", n_clicks=0, disabled=True), width="auto"),
            html.Div(id="spending-output")
        ], className="mt-3"),
        
        # 存储当前行数的状态组件
        dcc.Store(id="rows-count", data=1)
    ]), label="Expenses")
])))

# 渲染初始行和新增行
@callback(
    Output("input-rows-container", "children"),
    Input("rows-count", "data"),
    prevent_initial_call=False
)
def render_input_rows(rows_count):
    rows = []
    for i in range(rows_count):
        row = dbc.Row([
            dbc.Col(
                dcc.Dropdown(
                    id={'type': 'category-dropdown', 'index': i},
                    options=[{"label": x, "value": x} for x in ["Eating Out", "Groceries", "Entertainment"]],
                    value="Eating Out" if i == 0 else None
                ),
                lg=2
            ),
            dbc.Col(
                dbc.Input(id={'type': 'amount-spent', 'index': i}, type="number", value=0),
                lg=3
            ),
            dbc.Col(
                dbc.InputGroup([
                    dbc.InputGroupText("Tax Rate"),
                    dbc.Input(id={'type': 'tax-rate', 'index': i}, type="number", value=0)
                ]),
                lg=3
            ),
            dbc.Col(
                dbc.InputGroup([
                    dbc.InputGroupText("Total"),
                    dbc.Input(id={'type': 'total', 'index': i}, type="number", value=0, disabled=True)
                ]),
                lg=3
            ),
            dbc.Col(
                html.Button("×", id={'type': 'delete-row-btn', 'index': i}, n_clicks=0, className="btn btn-danger"),
                lg=1
            )
        ], className="mb-3")
        rows.append(row)
    return rows

# 计算每行的Total值
@callback(
    Output({'type': 'total', 'index': MATCH}, 'value'),
    Input({'type': 'amount-spent', 'index': MATCH}, 'value'),
    Input({'type': 'tax-rate', 'index': MATCH}, 'value'),
    prevent_initial_call=False
)
def update_row_total(amount, tax_rate):
    if amount is None or tax_rate is None:
        return 0
    return round(amount * (1 + (tax_rate / 100)), 2)

# 根据所有行的Total状态启用/禁用提交按钮
@callback(
    Output("submit-all-btn", "disabled"),
    Input({'type': 'total', 'index': ALL}, 'value'),
    prevent_initial_call=False
)
def toggle_submit_button(all_totals):
    return any(total <= 0 for total in all_totals)

# 添加行逻辑
@callback(
    Output("rows-count", "data"),
    Input("add-row-btn", "n_clicks"),
    State("rows-count", "data"),
    prevent_initial_call=False
)
def add_row(n_clicks, current_count):
    return current_count + n_clicks

# 删除行逻辑
@callback(
    Output("rows-count", "data"),
    Input({'type': 'delete-row-btn', 'index': ALL}, 'n_clicks'),
    State("rows-count", "data"),
    prevent_initial_call=True
)
def delete_row(delete_clicks, current_count):
    ctx = callback_context
    if not ctx.triggered:
        return current_count
    triggered_id = ctx.triggered[0]['prop_id'].split('.')[0]
    index = eval(triggered_id)['index']
    return max(current_count - 1, 1)

# 提交所有交易数据
@callback(
    Output("spending-output", "children"),
    Input("submit-all-btn", "n_clicks"),
    State({'type': 'category-dropdown', 'index': ALL}, 'value'),
    State({'type': 'total', 'index': ALL}, 'value'),
    prevent_initial_call=True
)
def submit_all(n_clicks, all_categories, all_totals):
    # 过滤未填写的行
    data = {
        'Category': [cat for cat in all_categories if cat is not None],
        'Amount': [total for total, cat in zip(all_totals, all_categories) if cat is not None]
    }
    df = pd.DataFrame(data)
    df.to_excel("spending_data.xlsx", index=False)
    return html.P("数据已成功保存!", className="text-success mt-2")

if __name__ == "__main__":
    app.run_server(debug=True)

关键说明

  • 模式匹配ID:采用{'type': 组件类型, 'index': 行号}的字典格式作为组件ID,MATCH用于匹配单行同类型组件,ALL用于匹配所有行的同类型组件,避免编写大量重复回调
  • 动态渲染:通过rows-count状态组件控制行数,每次添加/删除行时自动重新渲染所有输入行
  • 通用逻辑:仅需少量回调即可处理所有行的计算、状态更新,大幅减少代码冗余

内容的提问来源于stack exchange,提问作者Shawn Schreier

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.23 01:50:57