|
|
import streamlit as st |
|
|
import pandas as pd |
|
|
import requests |
|
|
import json |
|
|
from io import StringIO |
|
|
import plotly.express as px |
|
|
import qrcode |
|
|
from PIL import Image |
|
|
import io |
|
|
|
|
|
|
|
|
st.markdown(""" |
|
|
<style> |
|
|
.stApp header h1 { |
|
|
white-space: nowrap; /* 防止標題換行 */ |
|
|
overflow: hidden; |
|
|
text-overflow: ellipsis; |
|
|
font-size: 2rem; /* 調整字體大小為 2rem,約 32px */ |
|
|
} |
|
|
.stImage > img { |
|
|
max-height: 80vh; /* 圖片高度接近視窗高度 */ |
|
|
object-fit: contain; /* 保持比例 */ |
|
|
} |
|
|
/* 調整按鈕樣式以確保對稱 */ |
|
|
.stButton > button { |
|
|
width: 100px; /* 固定按鈕寬度 */ |
|
|
text-align: center; /* 文字居中 */ |
|
|
padding: 5px 10px; /* 調整內部間距 */ |
|
|
margin: 0 auto; /* 水平居中 */ |
|
|
} |
|
|
/* 確保列寬度均等 */ |
|
|
.st-ds { |
|
|
justify-content: space-between; /* 兩列間距均等 */ |
|
|
} |
|
|
/* 右下角資訊 */ |
|
|
.footer { |
|
|
position: fixed; |
|
|
bottom: 10px; |
|
|
right: 10px; |
|
|
font-size: 0.9rem; |
|
|
color: #666; |
|
|
padding: 5px; |
|
|
} |
|
|
/* 頒獎台樣式(正常顯示) */ |
|
|
.podium-container { |
|
|
display: flex; |
|
|
justify-content: center; |
|
|
align-items: flex-end; /* 確保下緣對齊 */ |
|
|
margin-top: 20px; |
|
|
margin-bottom: 20px; |
|
|
} |
|
|
.podium { |
|
|
text-align: center; |
|
|
width: 120px; |
|
|
margin: 0 10px; |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
} |
|
|
.podium-filler { |
|
|
background-color: #1a1a1a; /* 與背景同色 */ |
|
|
flex-grow: 0; |
|
|
} |
|
|
.podium-step { |
|
|
border: 2px solid #000; |
|
|
color: #333; |
|
|
font-weight: bold; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
flex-grow: 0; |
|
|
} |
|
|
.podium-step.first { |
|
|
height: 150px; /* 第一名完整高度 */ |
|
|
background-color: #ffd700; /* 金色 */ |
|
|
} |
|
|
.podium-step.second { |
|
|
height: 120px; /* 第二名高度 */ |
|
|
background-color: #c0c0c0; /* 銀色 */ |
|
|
} |
|
|
.podium-step.third { |
|
|
height: 90px; /* 第三名高度 */ |
|
|
background-color: #cd7f32; /* 銅色 */ |
|
|
} |
|
|
.podium-label { |
|
|
margin-top: 5px; |
|
|
font-size: 1rem; |
|
|
color: #fff; /* 文字改為白色 */ |
|
|
border: 2px solid #fff; /* 添加白色邊框 */ |
|
|
padding: 5px; /* 內距 */ |
|
|
background-color: rgba(0, 0, 0, 0.5); /* 半透明背景,提升可讀性 */ |
|
|
white-space: pre-line; /* 允許換行 */ |
|
|
} |
|
|
</style> |
|
|
""", unsafe_allow_html=True) |
|
|
|
|
|
|
|
|
st.title("探索圓周率與無限猴子定理的奇妙世界") |
|
|
st.write(""" |
|
|
歡迎參加這個有趣的活動!我們將一起探索圓周率 (π) 和無限猴子定理的奧秘,並透過幸運數字查詢系統,發現你選的數字在 π 中的秘密。 |
|
|
""") |
|
|
|
|
|
|
|
|
image_data = [ |
|
|
{"path": "m1.GIF", "caption": "圓周率與無限猴子定理的奇妙關聯"}, |
|
|
{"path": "m2.GIF", "caption": "重複與不重複"}, |
|
|
{"path": "m3.GIF", "caption": "圓周率裡藏著你的幸運號碼"}, |
|
|
{"path": "m4.GIF", "caption": "無限猴子定理是什麼?"}, |
|
|
{"path": "m5.GIF", "caption": "圓周率是無限長的數字打字機"}, |
|
|
] |
|
|
|
|
|
|
|
|
if 'page' not in st.session_state: |
|
|
st.session_state.page = 0 |
|
|
|
|
|
|
|
|
st.image(image_data[st.session_state.page]["path"], |
|
|
caption=image_data[st.session_state.page]["caption"], |
|
|
use_container_width=True) |
|
|
|
|
|
|
|
|
col1, col2 = st.columns(2) |
|
|
with col1: |
|
|
if st.button("上一頁", disabled=st.session_state.page <= 0): |
|
|
st.session_state.page -= 1 |
|
|
st.rerun() |
|
|
with col2: |
|
|
if st.button("下一頁", disabled=st.session_state.page >= len(image_data) - 1): |
|
|
st.session_state.page += 1 |
|
|
st.rerun() |
|
|
|
|
|
|
|
|
st.write(f"頁面: {st.session_state.page + 1} / {len(image_data)}") |
|
|
|
|
|
|
|
|
st.markdown("---") |
|
|
|
|
|
|
|
|
def process_number(number): |
|
|
"""查詢特定數字並返回結果""" |
|
|
url = f"https://www.angio.net/newpi/piquery?q={number}" |
|
|
try: |
|
|
response = requests.get(url) |
|
|
response.raise_for_status() |
|
|
data = response.json() |
|
|
|
|
|
|
|
|
if data.get('status') == 'OK' and data.get('r'): |
|
|
return data['r'][0].get('p') |
|
|
return None |
|
|
except Exception as e: |
|
|
st.error(f"處理數字 {number} 時發生錯誤: {str(e)}") |
|
|
return None |
|
|
|
|
|
def generate_qr_code(url): |
|
|
"""生成 QR Code 並返回圖片""" |
|
|
qr = qrcode.QRCode( |
|
|
version=1, |
|
|
error_correction=qrcode.constants.ERROR_CORRECT_L, |
|
|
box_size=10, |
|
|
border=4, |
|
|
) |
|
|
qr.add_data(url) |
|
|
qr.make(fit=True) |
|
|
|
|
|
img = qr.make_image(fill_color="black", back_color="white") |
|
|
return img |
|
|
|
|
|
def main(): |
|
|
st.title("幸運數字查詢系統") |
|
|
|
|
|
|
|
|
st.subheader("請輸入 Google 表單連結") |
|
|
form_url = st.text_input("表單連結", placeholder="請輸入 Google 表單的完整 URL") |
|
|
|
|
|
if form_url: |
|
|
try: |
|
|
|
|
|
if not form_url.startswith("https://"): |
|
|
st.error("請輸入有效的 URL(以 https:// 開頭)") |
|
|
else: |
|
|
st.write("以下是表單連結的 QR Code,學生可以使用平板掃描填寫:") |
|
|
|
|
|
qr_img = generate_qr_code(form_url) |
|
|
|
|
|
buf = io.BytesIO() |
|
|
qr_img.save(buf, format="PNG") |
|
|
byte_im = buf.getvalue() |
|
|
st.image(byte_im, caption="掃描此 QR Code 前往表單", use_container_width=False) |
|
|
except Exception as e: |
|
|
st.error(f"生成 QR Code 時發生錯誤: {str(e)}") |
|
|
|
|
|
|
|
|
st.markdown("---") |
|
|
|
|
|
|
|
|
selected_digits = st.selectbox( |
|
|
"請選擇幸運數字的位數", |
|
|
options=range(4, 11), |
|
|
index=3 |
|
|
) |
|
|
|
|
|
|
|
|
uploaded_file = st.file_uploader("請上傳CSV檔案", type=['csv']) |
|
|
|
|
|
if uploaded_file: |
|
|
try: |
|
|
|
|
|
df = pd.read_csv( |
|
|
StringIO(uploaded_file.getvalue().decode('utf-8')), |
|
|
header=0, |
|
|
dtype=str |
|
|
) |
|
|
|
|
|
|
|
|
required_columns = { |
|
|
"您的姓名": "姓名", |
|
|
"你的幸運號碼是?(可重複,前面也可以是0。Ex. 0000013、1111111)": "幸運號碼", |
|
|
"姓名": "姓名", |
|
|
"自選號碼": "幸運號碼" |
|
|
} |
|
|
|
|
|
|
|
|
available_columns = [col for col in required_columns.keys() if col in df.columns] |
|
|
if not available_columns or len(available_columns) < 2: |
|
|
|
|
|
df = pd.read_csv( |
|
|
StringIO(uploaded_file.getvalue().decode('utf-8')), |
|
|
header=None, |
|
|
dtype=str |
|
|
) |
|
|
if len(df.columns) >= 2: |
|
|
df.columns = ["姓名", "幸運號碼"] |
|
|
else: |
|
|
st.error("CSV 檔案格式錯誤,缺少 '姓名' 或 '幸運號碼' 欄位。") |
|
|
return |
|
|
|
|
|
|
|
|
df = df[[col for col in df.columns if col in required_columns]].rename(columns=required_columns) |
|
|
|
|
|
|
|
|
st.subheader("原始資料") |
|
|
st.dataframe(df) |
|
|
|
|
|
|
|
|
st.subheader("處理結果") |
|
|
|
|
|
|
|
|
progress_bar = st.progress(0) |
|
|
status_text = st.empty() |
|
|
|
|
|
|
|
|
df['查詢結果'] = None |
|
|
|
|
|
|
|
|
total_rows = len(df) |
|
|
for index, row in df.iterrows(): |
|
|
|
|
|
progress = (index + 1) / total_rows |
|
|
progress_bar.progress(progress) |
|
|
status_text.text(f"正在處理: {index + 1}/{total_rows}") |
|
|
|
|
|
|
|
|
original_number = str(row['幸運號碼']) |
|
|
number_length = len(original_number) |
|
|
|
|
|
if number_length > selected_digits: |
|
|
|
|
|
df.at[index, '幸運號碼'] = original_number |
|
|
df.at[index, '查詢結果'] = "數字不符規定" |
|
|
else: |
|
|
|
|
|
lucky_number = original_number.zfill(selected_digits) |
|
|
result = process_number(lucky_number) |
|
|
df.at[index, '幸運號碼'] = lucky_number |
|
|
df.at[index, '查詢結果'] = result |
|
|
|
|
|
|
|
|
st.subheader("完整結果") |
|
|
st.dataframe(df) |
|
|
|
|
|
|
|
|
csv = df.to_csv(index=False, encoding='utf-8') |
|
|
st.download_button( |
|
|
label="下載處理結果", |
|
|
data=csv, |
|
|
file_name="processed_results.csv", |
|
|
mime="text/csv" |
|
|
) |
|
|
|
|
|
|
|
|
st.subheader("查詢結果長條圖") |
|
|
|
|
|
valid_df = df[df['查詢結果'].notna() & (df['查詢結果'] != "數字不符規定")].copy() |
|
|
|
|
|
if not valid_df.empty: |
|
|
|
|
|
valid_df['查詢結果'] = valid_df['查詢結果'].astype(float) |
|
|
|
|
|
valid_df['姓名_幸運號碼'] = valid_df['姓名'] + "_" + valid_df['幸運號碼'] |
|
|
|
|
|
valid_df = valid_df.sort_values(by='查詢結果', ascending=False) |
|
|
|
|
|
|
|
|
fig = px.bar( |
|
|
valid_df, |
|
|
x='姓名_幸運號碼', |
|
|
y='查詢結果', |
|
|
labels={'姓名_幸運號碼': '姓名與幸運號碼', '查詢結果': '查詢結果數值'}, |
|
|
title="幸運數字查詢結果長條圖(按數值從大到小排序)" |
|
|
) |
|
|
|
|
|
fig.update_layout( |
|
|
xaxis_title="姓名與幸運號碼", |
|
|
yaxis_title="查詢結果數值", |
|
|
xaxis_tickangle=-45, |
|
|
margin=dict(l=50, r=50, t=80, b=150) |
|
|
) |
|
|
|
|
|
st.plotly_chart(fig, use_container_width=True) |
|
|
|
|
|
|
|
|
st.subheader("頒獎台:查詢結果前三名") |
|
|
|
|
|
top_3 = valid_df.head(3) |
|
|
|
|
|
if len(top_3) > 0: |
|
|
|
|
|
col1, col2, col3 = st.columns(3) |
|
|
|
|
|
with col1: |
|
|
if len(top_3) >= 2: |
|
|
second = top_3.iloc[1] |
|
|
st.markdown( |
|
|
f'<div class="podium">' |
|
|
f'<div class="podium-filler" style="height: 30px;"></div>' |
|
|
f'<div class="podium-step second">{int(second["查詢結果"])}</div>' |
|
|
f'<div class="podium-label">{second["姓名"]}\n{second["幸運號碼"]}</div>' |
|
|
f'</div>', |
|
|
unsafe_allow_html=True |
|
|
) |
|
|
else: |
|
|
st.markdown('<div class="podium"><div class="podium-filler" style="height: 30px;"></div><div class="podium-step second"></div><div class="podium-label"></div></div>', unsafe_allow_html=True) |
|
|
|
|
|
with col2: |
|
|
if len(top_3) >= 1: |
|
|
first = top_3.iloc[0] |
|
|
st.markdown( |
|
|
f'<div class="podium">' |
|
|
f'<div class="podium-step first">{int(first["查詢結果"])}</div>' |
|
|
f'<div class="podium-label">{first["姓名"]}\n{first["幸運號碼"]}</div>' |
|
|
f'</div>', |
|
|
unsafe_allow_html=True |
|
|
) |
|
|
else: |
|
|
st.markdown('<div class="podium"><div class="podium-step first"></div><div class="podium-label"></div></div>', unsafe_allow_html=True) |
|
|
|
|
|
with col3: |
|
|
if len(top_3) >= 3: |
|
|
third = top_3.iloc[2] |
|
|
st.markdown( |
|
|
f'<div class="podium">' |
|
|
f'<div class="podium-filler" style="height: 60px;"></div>' |
|
|
f'<div class="podium-step third">{int(third["查詢結果"])}</div>' |
|
|
f'<div class="podium-label">{third["姓名"]}\n{third["幸運號碼"]}</div>' |
|
|
f'</div>', |
|
|
unsafe_allow_html=True |
|
|
) |
|
|
else: |
|
|
st.markdown('<div class="podium"><div class="podium-filler" style="height: 60px;"></div><div class="podium-step third"></div><div class="podium-label"></div></div>', unsafe_allow_html=True) |
|
|
|
|
|
else: |
|
|
st.warning("無有效的查詢結果可供顯示頒獎台。") |
|
|
else: |
|
|
st.warning("無有效的查詢結果可供繪製長條圖或頒獎台。請確認資料是否正確。") |
|
|
|
|
|
except Exception as e: |
|
|
st.error(f"處理檔案時發生錯誤: {str(e)}") |
|
|
|
|
|
|
|
|
st.markdown('<div class="footer">程式設計:新竹縣立精華國中 藍星宇老師</div>', unsafe_allow_html=True) |
|
|
|
|
|
if __name__ == "__main__": |
|
|
main() |