import streamlit as st import pandas as pd def editable_dataframe_add(): st.title("🛠️ 可编辑DataFrame演示, num_rows='dynamic'") st.write("下面的表格可以直接编辑,尝试修改评分并查看你最喜欢的命令!") # 示例数据 df = pd.DataFrame( [ {"command": "st.selectbox", "rating": 4, "is_widget": True}, {"command": "st.balloons", "rating": 5, "is_widget": False}, {"command": "st.time_input", "rating": 3, "is_widget": True}, ] ) edited_df = st.data_editor(df, num_rows="dynamic") favorite_command = edited_df.loc[edited_df["rating"].idxmax()]["command"] st.markdown(f"Your favorite command is **{favorite_command}** 🎈") def editable_dataframe_add_fixed(): df = pd.DataFrame( [ {"command": "st.selectbox", "rating": 4, "is_widget": True}, {"command": "st.balloons", "rating": 5, "is_widget": False}, {"command": "st.time_input", "rating": 3, "is_widget": True}, ] ) edited_df = st.data_editor( df, column_config={ "command": "Streamlit Command", "rating": st.column_config.NumberColumn( "Your rating", help="How much do you like this command (1-5)?", min_value=1, max_value=5, step=1, format="%d ⭐", ), "is_widget": "Widget ?", }, disabled=["command", "is_widget"], hide_index=True, ) favorite_command = edited_df.loc[edited_df["rating"].idxmax()]["command"] st.markdown(f"Your favorite command is **{favorite_command}** 🎈") def editable_dataframe_basic(): st.title("✏️ 基础可编辑DataFrame") # 初始化数据 if 'edit_df' not in st.session_state: st.session_state.edit_df = pd.DataFrame({ 'Name': ['Alice', 'Bob', 'Charlie', 'Diana'], 'Age': [25, 30, 35, 28], 'City': ['New York', 'London', 'Tokyo', 'Paris'], 'Salary': [50000, 60000, 70000, 55000], 'Active': [True, False, True, True] }) st.write("📝 双击单元格即可编辑数据:") # 使用st.data_editor进行编辑 edited_df = st.data_editor( st.session_state.edit_df, height=300, use_container_width=True, num_rows="dynamic", # 允许添加/删除行 column_config={ "Name": st.column_config.TextColumn( "姓名", help="输入姓名", max_chars=50, validate="^[A-Za-z ]+$" # 只允许字母和空格 ), "Age": st.column_config.NumberColumn( "年龄", help="输入年龄 (18-100)", min_value=18, max_value=100, step=1, format="%d岁" ), "City": st.column_config.SelectboxColumn( "城市", help="选择城市", options=[ "New York", "London", "Tokyo", "Paris", "Beijing", "Sydney" ], required=True ), "Salary": st.column_config.NumberColumn( "薪资", help="输入薪资", min_value=0, max_value=1000000, step=1000, format="$%d" ), "Active": st.column_config.CheckboxColumn( "是否活跃", help="勾选表示活跃用户", default=True ) }, disabled=["Name"], # 禁用Name列的编辑 key="basic_editor" ) # 显示更改 if not edited_df.equals(st.session_state.edit_df): st.write("🔄 **数据已更改:**") col1, col2 = st.columns(2) with col1: st.write("**原始数据:**") st.dataframe(st.session_state.edit_df, use_container_width=True) with col2: st.write("**修改后数据:**") st.dataframe(edited_df, use_container_width=True) # 保存更改按钮 if st.button("💾 保存更改", type="primary"): st.session_state.edit_df = edited_df.copy() st.success("✅ 数据已保存!") st.rerun() if st.button("↩️ 撤销更改"): st.warning("❌ 已撤销更改") st.rerun() def editable_dataframe_advanced(): st.title("🔧 高级可编辑DataFrame") # 初始化复杂数据 if 'advanced_df' not in st.session_state: st.session_state.advanced_df = pd.DataFrame({ 'ID': range(1, 11), 'Product': [f'Product {i}' for i in range(1, 11)], 'Category': ['Electronics', 'Clothing', 'Food', 'Books', 'Toys'] * 2, 'Price': [19.99, 29.99, 9.99, 15.99, 25.99] * 2, 'Stock': [100, 50, 200, 75, 30, 120, 80, 150, 60, 90], 'Rating': [4.5, 3.8, 4.2, 4.7, 3.9, 4.1, 4.6, 4.3, 3.7, 4.4], 'Launch_Date': pd.date_range('2023-01-01', periods=10, freq='30D'), 'Image_URL': ['https://via.placeholder.com/50x50'] * 10, 'Available': [True, False, True, True, False, True, True, False, True, True] }) st.write("🛍️ 产品管理系统 - 高级编辑功能:") # 筛选选项 col1, col2, col3 = st.columns(3) with col1: category_filter = st.multiselect( "筛选类别", options=st.session_state.advanced_df['Category'].unique(), default=st.session_state.advanced_df['Category'].unique() ) with col2: price_range = st.slider( "价格范围", min_value=0.0, max_value=50.0, value=(0.0, 50.0), step=0.01 ) with col3: show_available_only = st.checkbox("只显示可用商品", value=False) # 应用筛选 filtered_df = st.session_state.advanced_df[ (st.session_state.advanced_df['Category'].isin(category_filter)) & (st.session_state.advanced_df['Price'] >= price_range[0]) & (st.session_state.advanced_df['Price'] <= price_range[1]) ] if show_available_only: filtered_df = filtered_df[filtered_df['Available'] == True] # 添加筛选提示 if len(filtered_df) < len(st.session_state.advanced_df): st.info(f"📊 当前显示 {len(filtered_df)} 行(共 {len(st.session_state.advanced_df)} 行)。在筛选状态下,新增行将添加到筛选结果的末尾。") # 选项:是否编辑完整数据 edit_mode = st.radio( "编辑模式", options=["筛选后数据", "完整数据"], help="选择'完整数据'可以在任意位置插入新行" ) if edit_mode == "筛选后数据": # 编辑筛选后的数据 display_df = filtered_df.copy() data_key = "filtered_editor" else: # 编辑完整数据 display_df = st.session_state.advanced_df.copy() data_key = "full_editor" # 高级编辑器 edited_df = st.data_editor( display_df, height=400, use_container_width=True, num_rows="dynamic", column_config={ "ID": st.column_config.NumberColumn( "ID", help="产品ID", disabled=True ), "Product": st.column_config.TextColumn( "产品名称", help="输入产品名称", pinned=True, # 固定列 max_chars=100 ), "Category": st.column_config.SelectboxColumn( "类别", help="选择产品类别", options=['Electronics', 'Clothing', 'Food', 'Books', 'Toys', 'Sports', 'Home'], required=True ), "Price": st.column_config.NumberColumn( "价格", help="输入价格", min_value=0.01, max_value=9999.99, step=0.01, format="$%.2f" ), "Stock": st.column_config.NumberColumn( "库存", help="输入库存数量", min_value=0, max_value=10000, step=1, format="%d件" ), "Rating": st.column_config.NumberColumn( "评分", help="产品评分 (1-5)", min_value=1.0, max_value=5.0, step=0.1, format="%.1f⭐" ), "Launch_Date": st.column_config.DateColumn( "上架日期", help="选择上架日期", min_value=pd.Timestamp('2020-01-01'), max_value=pd.Timestamp('2030-12-31'), format="YYYY-MM-DD" ), "Image_URL": st.column_config.ImageColumn( "产品图片", help="产品图片URL" ), "Available": st.column_config.CheckboxColumn( "可用", help="产品是否可用", default=True ) }, key=data_key ) # 处理数据变化 if not edited_df.equals(display_df): st.write("🔄 **检测到数据变化**") # 保存按钮 col_save, col_cancel = st.columns(2) with col_save: if st.button("💾 保存更改", type="primary"): if edit_mode == "完整数据": # 直接更新完整数据 st.session_state.advanced_df = edited_df.copy() else: # 筛选模式下的保存逻辑 # 需要将筛选后的更改合并回原始数据 # 这里简化处理:用户需要切换到完整数据模式进行编辑 st.warning("⚠️ 在筛选模式下只能添加新行,无法在指定位置插入。请切换到'完整数据'模式进行精确编辑。") # 只处理新增的行 new_rows = edited_df[~edited_df.index.isin(display_df.index)] if len(new_rows) > 0: # 为新行分配新的ID max_id = st.session_state.advanced_df['ID'].max() new_rows = new_rows.copy() new_rows['ID'] = range(max_id + 1, max_id + 1 + len(new_rows)) # 添加到原始数据末尾 st.session_state.advanced_df = pd.concat([st.session_state.advanced_df, new_rows], ignore_index=True) st.success(f"✅ 已添加 {len(new_rows)} 行新数据") else: st.session_state.advanced_df = edited_df.copy() st.success("✅ 数据已保存") st.rerun() with col_cancel: if st.button("↩️ 取消更改"): st.rerun() # 快速插入工具 st.subheader("⚡ 快速插入工具") with st.expander("🔧 精确位置插入", expanded=False): col1, col2, col3 = st.columns(3) with col1: insert_position = st.number_input( "插入位置(行号)", min_value=0, max_value=len(st.session_state.advanced_df), value=2, help="0=开头,2=第2行后" ) with col2: new_product_name = st.text_input("产品名称", value="New Product") new_category = st.selectbox("类别", ['Electronics', 'Clothing', 'Food', 'Books', 'Toys', 'Sports', 'Home']) with col3: new_price = st.number_input("价格", min_value=0.01, value=29.99) new_stock = st.number_input("库存", min_value=0, value=100) # 插入按钮 col_btn1, col_btn2, col_btn3 = st.columns(3) with col_btn1: if st.button("➕ 在指定位置插入"): new_row = pd.DataFrame({ 'ID': [st.session_state.advanced_df['ID'].max() + 1], 'Product': [new_product_name], 'Category': [new_category], 'Price': [new_price], 'Stock': [new_stock], 'Rating': [4.0], 'Launch_Date': [pd.Timestamp.now().date()], 'Image_URL': ['https://via.placeholder.com/50x50'], 'Available': [True] }) # 在指定位置插入 if insert_position == 0: st.session_state.advanced_df = pd.concat([new_row, st.session_state.advanced_df], ignore_index=True) elif insert_position >= len(st.session_state.advanced_df): st.session_state.advanced_df = pd.concat([st.session_state.advanced_df, new_row], ignore_index=True) else: df_before = st.session_state.advanced_df.iloc[:insert_position] df_after = st.session_state.advanced_df.iloc[insert_position:] st.session_state.advanced_df = pd.concat([df_before, new_row, df_after], ignore_index=True) st.success(f"✅ 已在位置 {insert_position} 插入新产品") st.rerun() with col_btn2: if st.button("📝 在第2-3行间插入"): new_row = pd.DataFrame({ 'ID': [st.session_state.advanced_df['ID'].max() + 1], 'Product': ['插入产品'], 'Category': ['Electronics'], 'Price': [35.99], 'Stock': [80], 'Rating': [4.2], 'Launch_Date': [pd.Timestamp.now().date()], 'Image_URL': ['https://via.placeholder.com/50x50'], 'Available': [True] }) # 在第2行后插入(索引2的位置) df_before = st.session_state.advanced_df.iloc[:2] df_after = st.session_state.advanced_df.iloc[2:] st.session_state.advanced_df = pd.concat([df_before, new_row, df_after], ignore_index=True) st.success("✅ 已在第2-3行间插入新产品") st.rerun() with col_btn3: if st.button("🔄 重新排序ID"): # 重新分配连续的ID st.session_state.advanced_df['ID'] = range(1, len(st.session_state.advanced_df) + 1) st.success("✅ ID已重新排序") st.rerun() # 批量操作 st.subheader("📋 批量操作") col1, col2, col3, col4 = st.columns([1,1,1,1]) with col1: if st.button("🛒 全部设为可用"): st.session_state.advanced_df['Available'] = True st.success("✅ 所有产品已设为可用") st.rerun() with col2: if st.button("❌ 全部设为不可用"): st.session_state.advanced_df['Available'] = False st.success("✅ 所有产品已设为不可用") st.rerun() with col3: discount_rate = st.number_input("折扣率 (%)", min_value=0, max_value=50, value=10, label_visibility="collapsed") if st.button("💰 应用折扣"): st.session_state.advanced_df['Price'] *= (1 - discount_rate / 100) st.success(f"✅ 已应用 {discount_rate}% 折扣") st.rerun() with col4: # # 修复方案1:使用session_state管理重置确认状态 # if 'reset_confirm' not in st.session_state: # st.session_state.reset_confirm = False # if st.button("📊 重置数据"): # st.session_state.reset_confirm = True # if st.session_state.reset_confirm: # st.warning("⚠️ 确认要重置所有数据吗?此操作不可撤销!") # if st.button("✅ 确认重置", type="primary"): # del st.session_state.advanced_df # st.session_state.reset_confirm = False # st.success("✅ 数据已重置") # st.rerun() # if st.button("❌ 取消"): # st.session_state.reset_confirm = False # st.rerun() # 方案2:使用expander + checkbox + button的组合 with st.expander("🗑️ 危险操作", expanded=False): if 'reset_confirm' not in st.session_state: st.session_state.reset_confirm = False st.warning("⚠️ 重置数据将删除所有当前数据!") confirm_reset = st.checkbox("我确认要重置所有数据", key="confirm_reset_checkbox") if st.button("📊 执行重置", disabled=not confirm_reset, type="primary"): if confirm_reset: # 重置数据 del st.session_state.advanced_df # 清除确认状态 st.session_state.reset_confirm = False st.success("✅ 数据已重置") st.rerun() else: st.session_state.reset_confirm = False st.error("❌ 请先确认重置操作") # 统计信息 st.subheader("📈 统计信息") col1, col2, col3, col4 = st.columns(4) with col1: st.metric("总产品数", len(st.session_state.advanced_df)) with col2: available_count = len(st.session_state.advanced_df[st.session_state.advanced_df['Available']]) st.metric("可用产品", available_count) with col3: avg_price = st.session_state.advanced_df['Price'].mean() st.metric("平均价格", f"${avg_price:.2f}") with col4: total_stock = st.session_state.advanced_df['Stock'].sum() st.metric("总库存", f"{total_stock:,}件") def editable_dataframe_with_validation(): st.title("✅ 带数据验证的可编辑DataFrame") # 初始化数据 if 'validation_df' not in st.session_state: st.session_state.validation_df = pd.DataFrame({ 'Email': ['user1@example.com', 'user2@example.com', 'user3@example.com'], 'Phone': ['+1-555-0123', '+1-555-0124', '+1-555-0125'], 'Website': ['https://www.example1.com', 'https://www.example2.com', 'https://www.example3.com'], 'Score': [85, 92, 78], 'Grade': ['B', 'A', 'C'] }) st.write("🔒 此表格包含数据验证规则:") # 显示验证规则 with st.expander("📋 查看验证规则"): st.markdown(""" - **Email**: 必须是有效的邮箱格式 - **Phone**: 必须是 +X-XXX-XXXX 格式 - **Website**: 必须是有效的URL格式 - **Score**: 必须在 0-100 之间 - **Grade**: 只能是 A, B, C, D, F """) # 带验证的编辑器 edited_df = st.data_editor( st.session_state.validation_df, height=300, use_container_width=True, column_config={ "Email": st.column_config.TextColumn( "邮箱", help="输入有效的邮箱地址", validate=r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$" ), "Phone": st.column_config.TextColumn( "电话", help="格式: +1-555-0123", validate=r"^\+\d{1,3}-\d{3}-\d{4}$" ), "Website": st.column_config.LinkColumn( "网站", help="输入完整的URL", validate=r"^https?://[^\s/$.?#].[^\s]*$" ), "Score": st.column_config.NumberColumn( "分数", help="输入 0-100 之间的分数", min_value=0, max_value=100, step=1 ), "Grade": st.column_config.SelectboxColumn( "等级", help="选择等级", options=['A', 'B', 'C', 'D', 'F'], required=True ) }, key="validation_editor" ) # 验证结果 if st.button("🔍 验证数据"): errors = [] # 邮箱验证 import re email_pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$" for idx, email in enumerate(edited_df['Email']): if not re.match(email_pattern, str(email)): errors.append(f"第{idx+1}行: 无效的邮箱格式") # 电话验证 phone_pattern = r"^\+\d{1,3}-\d{3}-\d{4}$" for idx, phone in enumerate(edited_df['Phone']): if not re.match(phone_pattern, str(phone)): errors.append(f"第{idx+1}行: 无效的电话格式") if errors: st.error("❌ 发现以下错误:") for error in errors: st.error(f"• {error}") else: st.success("✅ 所有数据验证通过!") page = st.radio("Select Page", [ "🔧 Editable DataFrame - add rows", "🔧 Editable DataFrame - add rows (fixed)", "🔧 Editable DataFrame - Basic", "🔧 Editable DataFrame - Advanced", "✅ Editable DataFrame - With Validation" ]) if page == "🔧 Editable DataFrame - add rows": editable_dataframe_add() elif page == "🔧 Editable DataFrame - add rows (fixed)": editable_dataframe_add_fixed() elif page == "🔧 Editable DataFrame - Basic": editable_dataframe_basic() elif page == "🔧 Editable DataFrame - Advanced": editable_dataframe_advanced() elif page == "✅ Editable DataFrame - With Validation": editable_dataframe_with_validation()