data_editor.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586
  1. import streamlit as st
  2. import pandas as pd
  3. def editable_dataframe_add():
  4. st.title("🛠️ 可编辑DataFrame演示, num_rows='dynamic'")
  5. st.write("下面的表格可以直接编辑,尝试修改评分并查看你最喜欢的命令!")
  6. # 示例数据
  7. df = pd.DataFrame(
  8. [
  9. {"command": "st.selectbox", "rating": 4, "is_widget": True},
  10. {"command": "st.balloons", "rating": 5, "is_widget": False},
  11. {"command": "st.time_input", "rating": 3, "is_widget": True},
  12. ]
  13. )
  14. edited_df = st.data_editor(df, num_rows="dynamic")
  15. favorite_command = edited_df.loc[edited_df["rating"].idxmax()]["command"]
  16. st.markdown(f"Your favorite command is **{favorite_command}** 🎈")
  17. def editable_dataframe_add_fixed():
  18. df = pd.DataFrame(
  19. [
  20. {"command": "st.selectbox", "rating": 4, "is_widget": True},
  21. {"command": "st.balloons", "rating": 5, "is_widget": False},
  22. {"command": "st.time_input", "rating": 3, "is_widget": True},
  23. ]
  24. )
  25. edited_df = st.data_editor(
  26. df,
  27. column_config={
  28. "command": "Streamlit Command",
  29. "rating": st.column_config.NumberColumn(
  30. "Your rating",
  31. help="How much do you like this command (1-5)?",
  32. min_value=1,
  33. max_value=5,
  34. step=1,
  35. format="%d ⭐",
  36. ),
  37. "is_widget": "Widget ?",
  38. },
  39. disabled=["command", "is_widget"],
  40. hide_index=True,
  41. )
  42. favorite_command = edited_df.loc[edited_df["rating"].idxmax()]["command"]
  43. st.markdown(f"Your favorite command is **{favorite_command}** 🎈")
  44. def editable_dataframe_basic():
  45. st.title("✏️ 基础可编辑DataFrame")
  46. # 初始化数据
  47. if 'edit_df' not in st.session_state:
  48. st.session_state.edit_df = pd.DataFrame({
  49. 'Name': ['Alice', 'Bob', 'Charlie', 'Diana'],
  50. 'Age': [25, 30, 35, 28],
  51. 'City': ['New York', 'London', 'Tokyo', 'Paris'],
  52. 'Salary': [50000, 60000, 70000, 55000],
  53. 'Active': [True, False, True, True]
  54. })
  55. st.write("📝 双击单元格即可编辑数据:")
  56. # 使用st.data_editor进行编辑
  57. edited_df = st.data_editor(
  58. st.session_state.edit_df,
  59. height=300,
  60. use_container_width=True,
  61. num_rows="dynamic", # 允许添加/删除行
  62. column_config={
  63. "Name": st.column_config.TextColumn(
  64. "姓名",
  65. help="输入姓名",
  66. max_chars=50,
  67. validate="^[A-Za-z ]+$" # 只允许字母和空格
  68. ),
  69. "Age": st.column_config.NumberColumn(
  70. "年龄",
  71. help="输入年龄 (18-100)",
  72. min_value=18,
  73. max_value=100,
  74. step=1,
  75. format="%d岁"
  76. ),
  77. "City": st.column_config.SelectboxColumn(
  78. "城市",
  79. help="选择城市",
  80. options=[
  81. "New York",
  82. "London",
  83. "Tokyo",
  84. "Paris",
  85. "Beijing",
  86. "Sydney"
  87. ],
  88. required=True
  89. ),
  90. "Salary": st.column_config.NumberColumn(
  91. "薪资",
  92. help="输入薪资",
  93. min_value=0,
  94. max_value=1000000,
  95. step=1000,
  96. format="$%d"
  97. ),
  98. "Active": st.column_config.CheckboxColumn(
  99. "是否活跃",
  100. help="勾选表示活跃用户",
  101. default=True
  102. )
  103. },
  104. disabled=["Name"], # 禁用Name列的编辑
  105. key="basic_editor"
  106. )
  107. # 显示更改
  108. if not edited_df.equals(st.session_state.edit_df):
  109. st.write("🔄 **数据已更改:**")
  110. col1, col2 = st.columns(2)
  111. with col1:
  112. st.write("**原始数据:**")
  113. st.dataframe(st.session_state.edit_df, use_container_width=True)
  114. with col2:
  115. st.write("**修改后数据:**")
  116. st.dataframe(edited_df, use_container_width=True)
  117. # 保存更改按钮
  118. if st.button("💾 保存更改", type="primary"):
  119. st.session_state.edit_df = edited_df.copy()
  120. st.success("✅ 数据已保存!")
  121. st.rerun()
  122. if st.button("↩️ 撤销更改"):
  123. st.warning("❌ 已撤销更改")
  124. st.rerun()
  125. def editable_dataframe_advanced():
  126. st.title("🔧 高级可编辑DataFrame")
  127. # 初始化复杂数据
  128. if 'advanced_df' not in st.session_state:
  129. st.session_state.advanced_df = pd.DataFrame({
  130. 'ID': range(1, 11),
  131. 'Product': [f'Product {i}' for i in range(1, 11)],
  132. 'Category': ['Electronics', 'Clothing', 'Food', 'Books', 'Toys'] * 2,
  133. 'Price': [19.99, 29.99, 9.99, 15.99, 25.99] * 2,
  134. 'Stock': [100, 50, 200, 75, 30, 120, 80, 150, 60, 90],
  135. 'Rating': [4.5, 3.8, 4.2, 4.7, 3.9, 4.1, 4.6, 4.3, 3.7, 4.4],
  136. 'Launch_Date': pd.date_range('2023-01-01', periods=10, freq='30D'),
  137. 'Image_URL': ['https://via.placeholder.com/50x50'] * 10,
  138. 'Available': [True, False, True, True, False, True, True, False, True, True]
  139. })
  140. st.write("🛍️ 产品管理系统 - 高级编辑功能:")
  141. # 筛选选项
  142. col1, col2, col3 = st.columns(3)
  143. with col1:
  144. category_filter = st.multiselect(
  145. "筛选类别",
  146. options=st.session_state.advanced_df['Category'].unique(),
  147. default=st.session_state.advanced_df['Category'].unique()
  148. )
  149. with col2:
  150. price_range = st.slider(
  151. "价格范围",
  152. min_value=0.0,
  153. max_value=50.0,
  154. value=(0.0, 50.0),
  155. step=0.01
  156. )
  157. with col3:
  158. show_available_only = st.checkbox("只显示可用商品", value=False)
  159. # 应用筛选
  160. filtered_df = st.session_state.advanced_df[
  161. (st.session_state.advanced_df['Category'].isin(category_filter)) &
  162. (st.session_state.advanced_df['Price'] >= price_range[0]) &
  163. (st.session_state.advanced_df['Price'] <= price_range[1])
  164. ]
  165. if show_available_only:
  166. filtered_df = filtered_df[filtered_df['Available'] == True]
  167. # 添加筛选提示
  168. if len(filtered_df) < len(st.session_state.advanced_df):
  169. st.info(f"📊 当前显示 {len(filtered_df)} 行(共 {len(st.session_state.advanced_df)} 行)。在筛选状态下,新增行将添加到筛选结果的末尾。")
  170. # 选项:是否编辑完整数据
  171. edit_mode = st.radio(
  172. "编辑模式",
  173. options=["筛选后数据", "完整数据"],
  174. help="选择'完整数据'可以在任意位置插入新行"
  175. )
  176. if edit_mode == "筛选后数据":
  177. # 编辑筛选后的数据
  178. display_df = filtered_df.copy()
  179. data_key = "filtered_editor"
  180. else:
  181. # 编辑完整数据
  182. display_df = st.session_state.advanced_df.copy()
  183. data_key = "full_editor"
  184. # 高级编辑器
  185. edited_df = st.data_editor(
  186. display_df,
  187. height=400,
  188. use_container_width=True,
  189. num_rows="dynamic",
  190. column_config={
  191. "ID": st.column_config.NumberColumn(
  192. "ID",
  193. help="产品ID",
  194. disabled=True
  195. ),
  196. "Product": st.column_config.TextColumn(
  197. "产品名称",
  198. help="输入产品名称",
  199. pinned=True, # 固定列
  200. max_chars=100
  201. ),
  202. "Category": st.column_config.SelectboxColumn(
  203. "类别",
  204. help="选择产品类别",
  205. options=['Electronics', 'Clothing', 'Food', 'Books', 'Toys', 'Sports', 'Home'],
  206. required=True
  207. ),
  208. "Price": st.column_config.NumberColumn(
  209. "价格",
  210. help="输入价格",
  211. min_value=0.01,
  212. max_value=9999.99,
  213. step=0.01,
  214. format="$%.2f"
  215. ),
  216. "Stock": st.column_config.NumberColumn(
  217. "库存",
  218. help="输入库存数量",
  219. min_value=0,
  220. max_value=10000,
  221. step=1,
  222. format="%d件"
  223. ),
  224. "Rating": st.column_config.NumberColumn(
  225. "评分",
  226. help="产品评分 (1-5)",
  227. min_value=1.0,
  228. max_value=5.0,
  229. step=0.1,
  230. format="%.1f⭐"
  231. ),
  232. "Launch_Date": st.column_config.DateColumn(
  233. "上架日期",
  234. help="选择上架日期",
  235. min_value=pd.Timestamp('2020-01-01'),
  236. max_value=pd.Timestamp('2030-12-31'),
  237. format="YYYY-MM-DD"
  238. ),
  239. "Image_URL": st.column_config.ImageColumn(
  240. "产品图片",
  241. help="产品图片URL"
  242. ),
  243. "Available": st.column_config.CheckboxColumn(
  244. "可用",
  245. help="产品是否可用",
  246. default=True
  247. )
  248. },
  249. key=data_key
  250. )
  251. # 处理数据变化
  252. if not edited_df.equals(display_df):
  253. st.write("🔄 **检测到数据变化**")
  254. # 保存按钮
  255. col_save, col_cancel = st.columns(2)
  256. with col_save:
  257. if st.button("💾 保存更改", type="primary"):
  258. if edit_mode == "完整数据":
  259. # 直接更新完整数据
  260. st.session_state.advanced_df = edited_df.copy()
  261. else:
  262. # 筛选模式下的保存逻辑
  263. # 需要将筛选后的更改合并回原始数据
  264. # 这里简化处理:用户需要切换到完整数据模式进行编辑
  265. st.warning("⚠️ 在筛选模式下只能添加新行,无法在指定位置插入。请切换到'完整数据'模式进行精确编辑。")
  266. # 只处理新增的行
  267. new_rows = edited_df[~edited_df.index.isin(display_df.index)]
  268. if len(new_rows) > 0:
  269. # 为新行分配新的ID
  270. max_id = st.session_state.advanced_df['ID'].max()
  271. new_rows = new_rows.copy()
  272. new_rows['ID'] = range(max_id + 1, max_id + 1 + len(new_rows))
  273. # 添加到原始数据末尾
  274. st.session_state.advanced_df = pd.concat([st.session_state.advanced_df, new_rows], ignore_index=True)
  275. st.success(f"✅ 已添加 {len(new_rows)} 行新数据")
  276. else:
  277. st.session_state.advanced_df = edited_df.copy()
  278. st.success("✅ 数据已保存")
  279. st.rerun()
  280. with col_cancel:
  281. if st.button("↩️ 取消更改"):
  282. st.rerun()
  283. # 快速插入工具
  284. st.subheader("⚡ 快速插入工具")
  285. with st.expander("🔧 精确位置插入", expanded=False):
  286. col1, col2, col3 = st.columns(3)
  287. with col1:
  288. insert_position = st.number_input(
  289. "插入位置(行号)",
  290. min_value=0,
  291. max_value=len(st.session_state.advanced_df),
  292. value=2,
  293. help="0=开头,2=第2行后"
  294. )
  295. with col2:
  296. new_product_name = st.text_input("产品名称", value="New Product")
  297. new_category = st.selectbox("类别", ['Electronics', 'Clothing', 'Food', 'Books', 'Toys', 'Sports', 'Home'])
  298. with col3:
  299. new_price = st.number_input("价格", min_value=0.01, value=29.99)
  300. new_stock = st.number_input("库存", min_value=0, value=100)
  301. # 插入按钮
  302. col_btn1, col_btn2, col_btn3 = st.columns(3)
  303. with col_btn1:
  304. if st.button("➕ 在指定位置插入"):
  305. new_row = pd.DataFrame({
  306. 'ID': [st.session_state.advanced_df['ID'].max() + 1],
  307. 'Product': [new_product_name],
  308. 'Category': [new_category],
  309. 'Price': [new_price],
  310. 'Stock': [new_stock],
  311. 'Rating': [4.0],
  312. 'Launch_Date': [pd.Timestamp.now().date()],
  313. 'Image_URL': ['https://via.placeholder.com/50x50'],
  314. 'Available': [True]
  315. })
  316. # 在指定位置插入
  317. if insert_position == 0:
  318. st.session_state.advanced_df = pd.concat([new_row, st.session_state.advanced_df], ignore_index=True)
  319. elif insert_position >= len(st.session_state.advanced_df):
  320. st.session_state.advanced_df = pd.concat([st.session_state.advanced_df, new_row], ignore_index=True)
  321. else:
  322. df_before = st.session_state.advanced_df.iloc[:insert_position]
  323. df_after = st.session_state.advanced_df.iloc[insert_position:]
  324. st.session_state.advanced_df = pd.concat([df_before, new_row, df_after], ignore_index=True)
  325. st.success(f"✅ 已在位置 {insert_position} 插入新产品")
  326. st.rerun()
  327. with col_btn2:
  328. if st.button("📝 在第2-3行间插入"):
  329. new_row = pd.DataFrame({
  330. 'ID': [st.session_state.advanced_df['ID'].max() + 1],
  331. 'Product': ['插入产品'],
  332. 'Category': ['Electronics'],
  333. 'Price': [35.99],
  334. 'Stock': [80],
  335. 'Rating': [4.2],
  336. 'Launch_Date': [pd.Timestamp.now().date()],
  337. 'Image_URL': ['https://via.placeholder.com/50x50'],
  338. 'Available': [True]
  339. })
  340. # 在第2行后插入(索引2的位置)
  341. df_before = st.session_state.advanced_df.iloc[:2]
  342. df_after = st.session_state.advanced_df.iloc[2:]
  343. st.session_state.advanced_df = pd.concat([df_before, new_row, df_after], ignore_index=True)
  344. st.success("✅ 已在第2-3行间插入新产品")
  345. st.rerun()
  346. with col_btn3:
  347. if st.button("🔄 重新排序ID"):
  348. # 重新分配连续的ID
  349. st.session_state.advanced_df['ID'] = range(1, len(st.session_state.advanced_df) + 1)
  350. st.success("✅ ID已重新排序")
  351. st.rerun()
  352. # 批量操作
  353. st.subheader("📋 批量操作")
  354. col1, col2, col3, col4 = st.columns([1,1,1,1])
  355. with col1:
  356. if st.button("🛒 全部设为可用"):
  357. st.session_state.advanced_df['Available'] = True
  358. st.success("✅ 所有产品已设为可用")
  359. st.rerun()
  360. with col2:
  361. if st.button("❌ 全部设为不可用"):
  362. st.session_state.advanced_df['Available'] = False
  363. st.success("✅ 所有产品已设为不可用")
  364. st.rerun()
  365. with col3:
  366. discount_rate = st.number_input("折扣率 (%)", min_value=0, max_value=50, value=10, label_visibility="collapsed")
  367. if st.button("💰 应用折扣"):
  368. st.session_state.advanced_df['Price'] *= (1 - discount_rate / 100)
  369. st.success(f"✅ 已应用 {discount_rate}% 折扣")
  370. st.rerun()
  371. with col4:
  372. # # 修复方案1:使用session_state管理重置确认状态
  373. # if 'reset_confirm' not in st.session_state:
  374. # st.session_state.reset_confirm = False
  375. # if st.button("📊 重置数据"):
  376. # st.session_state.reset_confirm = True
  377. # if st.session_state.reset_confirm:
  378. # st.warning("⚠️ 确认要重置所有数据吗?此操作不可撤销!")
  379. # if st.button("✅ 确认重置", type="primary"):
  380. # del st.session_state.advanced_df
  381. # st.session_state.reset_confirm = False
  382. # st.success("✅ 数据已重置")
  383. # st.rerun()
  384. # if st.button("❌ 取消"):
  385. # st.session_state.reset_confirm = False
  386. # st.rerun()
  387. # 方案2:使用expander + checkbox + button的组合
  388. with st.expander("🗑️ 危险操作", expanded=False):
  389. if 'reset_confirm' not in st.session_state:
  390. st.session_state.reset_confirm = False
  391. st.warning("⚠️ 重置数据将删除所有当前数据!")
  392. confirm_reset = st.checkbox("我确认要重置所有数据", key="confirm_reset_checkbox")
  393. if st.button("📊 执行重置", disabled=not confirm_reset, type="primary"):
  394. if confirm_reset:
  395. # 重置数据
  396. del st.session_state.advanced_df
  397. # 清除确认状态
  398. st.session_state.reset_confirm = False
  399. st.success("✅ 数据已重置")
  400. st.rerun()
  401. else:
  402. st.session_state.reset_confirm = False
  403. st.error("❌ 请先确认重置操作")
  404. # 统计信息
  405. st.subheader("📈 统计信息")
  406. col1, col2, col3, col4 = st.columns(4)
  407. with col1:
  408. st.metric("总产品数", len(st.session_state.advanced_df))
  409. with col2:
  410. available_count = len(st.session_state.advanced_df[st.session_state.advanced_df['Available']])
  411. st.metric("可用产品", available_count)
  412. with col3:
  413. avg_price = st.session_state.advanced_df['Price'].mean()
  414. st.metric("平均价格", f"${avg_price:.2f}")
  415. with col4:
  416. total_stock = st.session_state.advanced_df['Stock'].sum()
  417. st.metric("总库存", f"{total_stock:,}件")
  418. def editable_dataframe_with_validation():
  419. st.title("✅ 带数据验证的可编辑DataFrame")
  420. # 初始化数据
  421. if 'validation_df' not in st.session_state:
  422. st.session_state.validation_df = pd.DataFrame({
  423. 'Email': ['user1@example.com', 'user2@example.com', 'user3@example.com'],
  424. 'Phone': ['+1-555-0123', '+1-555-0124', '+1-555-0125'],
  425. 'Website': ['https://www.example1.com', 'https://www.example2.com', 'https://www.example3.com'],
  426. 'Score': [85, 92, 78],
  427. 'Grade': ['B', 'A', 'C']
  428. })
  429. st.write("🔒 此表格包含数据验证规则:")
  430. # 显示验证规则
  431. with st.expander("📋 查看验证规则"):
  432. st.markdown("""
  433. - **Email**: 必须是有效的邮箱格式
  434. - **Phone**: 必须是 +X-XXX-XXXX 格式
  435. - **Website**: 必须是有效的URL格式
  436. - **Score**: 必须在 0-100 之间
  437. - **Grade**: 只能是 A, B, C, D, F
  438. """)
  439. # 带验证的编辑器
  440. edited_df = st.data_editor(
  441. st.session_state.validation_df,
  442. height=300,
  443. use_container_width=True,
  444. column_config={
  445. "Email": st.column_config.TextColumn(
  446. "邮箱",
  447. help="输入有效的邮箱地址",
  448. validate=r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
  449. ),
  450. "Phone": st.column_config.TextColumn(
  451. "电话",
  452. help="格式: +1-555-0123",
  453. validate=r"^\+\d{1,3}-\d{3}-\d{4}$"
  454. ),
  455. "Website": st.column_config.LinkColumn(
  456. "网站",
  457. help="输入完整的URL",
  458. validate=r"^https?://[^\s/$.?#].[^\s]*$"
  459. ),
  460. "Score": st.column_config.NumberColumn(
  461. "分数",
  462. help="输入 0-100 之间的分数",
  463. min_value=0,
  464. max_value=100,
  465. step=1
  466. ),
  467. "Grade": st.column_config.SelectboxColumn(
  468. "等级",
  469. help="选择等级",
  470. options=['A', 'B', 'C', 'D', 'F'],
  471. required=True
  472. )
  473. },
  474. key="validation_editor"
  475. )
  476. # 验证结果
  477. if st.button("🔍 验证数据"):
  478. errors = []
  479. # 邮箱验证
  480. import re
  481. email_pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
  482. for idx, email in enumerate(edited_df['Email']):
  483. if not re.match(email_pattern, str(email)):
  484. errors.append(f"第{idx+1}行: 无效的邮箱格式")
  485. # 电话验证
  486. phone_pattern = r"^\+\d{1,3}-\d{3}-\d{4}$"
  487. for idx, phone in enumerate(edited_df['Phone']):
  488. if not re.match(phone_pattern, str(phone)):
  489. errors.append(f"第{idx+1}行: 无效的电话格式")
  490. if errors:
  491. st.error("❌ 发现以下错误:")
  492. for error in errors:
  493. st.error(f"• {error}")
  494. else:
  495. st.success("✅ 所有数据验证通过!")
  496. page = st.radio("Select Page", [
  497. "🔧 Editable DataFrame - add rows",
  498. "🔧 Editable DataFrame - add rows (fixed)",
  499. "🔧 Editable DataFrame - Basic",
  500. "🔧 Editable DataFrame - Advanced",
  501. "✅ Editable DataFrame - With Validation"
  502. ])
  503. if page == "🔧 Editable DataFrame - add rows":
  504. editable_dataframe_add()
  505. elif page == "🔧 Editable DataFrame - add rows (fixed)":
  506. editable_dataframe_add_fixed()
  507. elif page == "🔧 Editable DataFrame - Basic":
  508. editable_dataframe_basic()
  509. elif page == "🔧 Editable DataFrame - Advanced":
  510. editable_dataframe_advanced()
  511. elif page == "✅ Editable DataFrame - With Validation":
  512. editable_dataframe_with_validation()