如何在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
相关产品推荐
相关产品推荐

