Update app.py
Browse files
app.py
CHANGED
|
@@ -29,9 +29,9 @@ def get_bookings():
|
|
| 29 |
except:
|
| 30 |
return pd.DataFrame()
|
| 31 |
|
| 32 |
-
# 🔥🔥🔥
|
| 33 |
def send_confirmation_hybrid(booking_id):
|
| 34 |
-
print(f"收到後端請求,ID: {booking_id}")
|
| 35 |
if not booking_id: return "❌ 錯誤:未讀取到 ID (Backend received None)"
|
| 36 |
|
| 37 |
try:
|
|
@@ -49,11 +49,10 @@ def send_confirmation_hybrid(booking_id):
|
|
| 49 |
|
| 50 |
log_msg = f"🆔 {booking_id}: "
|
| 51 |
|
| 52 |
-
#
|
| 53 |
is_reminder = "確認" in current_status
|
| 54 |
|
| 55 |
if is_reminder:
|
| 56 |
-
# 提醒信
|
| 57 |
action_label = "提醒"
|
| 58 |
mail_subject = f"🔔 訂位提醒: {booking['date']} - Cié Cié Taipei"
|
| 59 |
line_text = (
|
|
@@ -67,7 +66,7 @@ def send_confirmation_hybrid(booking_id):
|
|
| 67 |
mail_html = f"""
|
| 68 |
<div style="background:#1a1a1a; color:#d4af37; padding:20px; border-radius:8px;">
|
| 69 |
<h2 style="text-align:center; border-bottom:1px solid #444;">Cié Cié Taipei</h2>
|
| 70 |
-
<p style="color:#eee;"><strong>{booking['name']}</strong>
|
| 71 |
<div style="background:#2a2a2a; padding:15px; margin:20px 0; border-left:4px solid #f1c40f;">
|
| 72 |
<ul style="color:#ddd;">
|
| 73 |
<li>📅 日期:{booking['date']}</li>
|
|
@@ -81,7 +80,6 @@ def send_confirmation_hybrid(booking_id):
|
|
| 81 |
</div>
|
| 82 |
"""
|
| 83 |
else:
|
| 84 |
-
# 確認信
|
| 85 |
action_label = "確認"
|
| 86 |
mail_subject = f"訂位確認: {booking['date']} - Cié Cié Taipei"
|
| 87 |
line_text = (
|
|
@@ -136,7 +134,7 @@ def send_confirmation_hybrid(booking_id):
|
|
| 136 |
|
| 137 |
except Exception as e: return f"🔥 嚴重錯誤: {str(e)}"
|
| 138 |
|
| 139 |
-
#
|
| 140 |
def render_booking_cards():
|
| 141 |
df = get_bookings()
|
| 142 |
count_html = f"<div style='color:#bbb; margin-bottom:20px; text-align:right; font-size:16px; padding: 0 10px;'>📊 共找到 <span style='color:#fff; font-weight:bold;'>{len(df)}</span> 筆資料</div>"
|
|
@@ -147,7 +145,6 @@ def render_booking_cards():
|
|
| 147 |
for index, row in df.iterrows():
|
| 148 |
status = row.get('status', '待處理')
|
| 149 |
|
| 150 |
-
# 狀態與顏色
|
| 151 |
status_color = "#ccc"; border_color = "#444"
|
| 152 |
if '確認' in status: status_color = "#2ecc71"; border_color = "#2ecc71"
|
| 153 |
elif '取消' in status: status_color = "#e74c3c"; border_color = "#e74c3c"
|
|
@@ -156,10 +153,10 @@ def render_booking_cards():
|
|
| 156 |
is_canceled = '取消' in status
|
| 157 |
is_confirmed = '確認' in status
|
| 158 |
|
| 159 |
-
#
|
| 160 |
-
|
|
|
|
| 161 |
|
| 162 |
-
# 按鈕外觀
|
| 163 |
if is_canceled:
|
| 164 |
btn_style = "background: #333; color: #666; cursor: not-allowed;"
|
| 165 |
btn_text = "🚫 已取消"
|
|
@@ -224,7 +221,10 @@ def render_booking_cards():
|
|
| 224 |
<div style="font-size: 1.1em; color: #000; font-weight: 900; background: #e0e0e0; padding: 8px 12px; border-radius: 6px; font-family: monospace;">
|
| 225 |
ID: {row['id']}
|
| 226 |
</div>
|
| 227 |
-
|
|
|
|
|
|
|
|
|
|
| 228 |
{btn_text}
|
| 229 |
</button>
|
| 230 |
</div>
|
|
@@ -245,15 +245,62 @@ def check_login(user, password):
|
|
| 245 |
}
|
| 246 |
else: return {error_msg: "<span style='color: red'>❌ 帳號或密碼錯誤</span>"}
|
| 247 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 248 |
|
| 249 |
-
# --- CSS ---
|
| 250 |
custom_css = """
|
| 251 |
body, .gradio-container { background-color: #0F0F0F; color: #fff; }
|
| 252 |
#booking_display { height: auto !important; max-height: none !important; overflow: visible !important; margin-bottom: 50px; }
|
| 253 |
button:active { transform: scale(0.96); }
|
| 254 |
#header-panel { background: #1a1a1a; padding: 15px; margin-bottom: 20px; border-radius: 10px; }
|
| 255 |
|
| 256 |
-
/*
|
|
|
|
|
|
|
|
|
|
| 257 |
#hidden_ops {
|
| 258 |
position: absolute !important;
|
| 259 |
left: -9999px !important;
|
|
@@ -264,50 +311,10 @@ button:active { transform: scale(0.96); }
|
|
| 264 |
}
|
| 265 |
"""
|
| 266 |
|
| 267 |
-
# 🔥🔥🔥 暴力注入 JS:直接寫在 HTML 裡,確保 cardAction 絕對全域可用 🔥🔥🔥
|
| 268 |
-
# 這段代碼會被直接渲染到網頁頂部,解決 ReferenceError
|
| 269 |
-
GLOBAL_JS = """
|
| 270 |
-
<script>
|
| 271 |
-
console.log("✨ 訂位系統腳本已載入 (v5.1)");
|
| 272 |
-
|
| 273 |
-
// 直接定義在 window 物件上,保證全域可見
|
| 274 |
-
window.cardAction = function(id) {
|
| 275 |
-
console.log("👉 按下 ID:", id);
|
| 276 |
-
|
| 277 |
-
// 1. 找輸入框 (相容 input 和 textarea)
|
| 278 |
-
let idInput = document.querySelector('#hidden_id_input input');
|
| 279 |
-
|
| 280 |
-
if (idInput) {
|
| 281 |
-
console.log("✅ 找到隱藏輸入框,寫入 ID...");
|
| 282 |
-
// 強制寫入值
|
| 283 |
-
idInput.value = id;
|
| 284 |
-
// 觸發 React/Gradio 的更新事件
|
| 285 |
-
let event = new Event('input', { bubbles: true });
|
| 286 |
-
idInput.dispatchEvent(event);
|
| 287 |
-
|
| 288 |
-
// 2. 觸發按鈕
|
| 289 |
-
setTimeout(() => {
|
| 290 |
-
let sendBtn = document.querySelector('#hidden_send_btn');
|
| 291 |
-
if (sendBtn) {
|
| 292 |
-
console.log("🚀 觸發發送按鈕...");
|
| 293 |
-
sendBtn.click();
|
| 294 |
-
} else {
|
| 295 |
-
console.error("❌ 找不到發送按鈕 (#hidden_send_btn)");
|
| 296 |
-
alert("系統錯誤:找不到發送按鈕,請重新整理頁面");
|
| 297 |
-
}
|
| 298 |
-
}, 200); // 延遲 200ms 確保數值已更新
|
| 299 |
-
} else {
|
| 300 |
-
console.error("❌ 找不到隱藏輸入框 (#hidden_id_input)");
|
| 301 |
-
alert("系統錯誤:找不到輸入框,請重新整理頁面");
|
| 302 |
-
}
|
| 303 |
-
}
|
| 304 |
-
</script>
|
| 305 |
-
"""
|
| 306 |
-
|
| 307 |
# --- 介面 ---
|
| 308 |
with gr.Blocks(title="Admin") as demo:
|
| 309 |
|
| 310 |
-
# 注入 JS
|
| 311 |
gr.HTML(GLOBAL_JS)
|
| 312 |
|
| 313 |
with gr.Group(visible=True) as login_row:
|
|
@@ -325,7 +332,7 @@ with gr.Blocks(title="Admin") as demo:
|
|
| 325 |
|
| 326 |
booking_display = gr.HTML(elem_id="booking_display")
|
| 327 |
|
| 328 |
-
#
|
| 329 |
with gr.Column(visible=True, elem_id="hidden_ops"):
|
| 330 |
hidden_id_input = gr.Number(elem_id="hidden_id_input", precision=0)
|
| 331 |
hidden_send_btn = gr.Button("Send", elem_id="hidden_send_btn")
|
|
|
|
| 29 |
except:
|
| 30 |
return pd.DataFrame()
|
| 31 |
|
| 32 |
+
# 🔥🔥🔥 發送邏輯 (暴力發送) 🔥🔥🔥
|
| 33 |
def send_confirmation_hybrid(booking_id):
|
| 34 |
+
print(f"收到後端請求,ID: {booking_id}")
|
| 35 |
if not booking_id: return "❌ 錯誤:未讀取到 ID (Backend received None)"
|
| 36 |
|
| 37 |
try:
|
|
|
|
| 49 |
|
| 50 |
log_msg = f"🆔 {booking_id}: "
|
| 51 |
|
| 52 |
+
# 判斷是提醒還是確認
|
| 53 |
is_reminder = "確認" in current_status
|
| 54 |
|
| 55 |
if is_reminder:
|
|
|
|
| 56 |
action_label = "提醒"
|
| 57 |
mail_subject = f"🔔 訂位提醒: {booking['date']} - Cié Cié Taipei"
|
| 58 |
line_text = (
|
|
|
|
| 66 |
mail_html = f"""
|
| 67 |
<div style="background:#1a1a1a; color:#d4af37; padding:20px; border-radius:8px;">
|
| 68 |
<h2 style="text-align:center; border-bottom:1px solid #444;">Cié Cié Taipei</h2>
|
| 69 |
+
<p style="color:#eee;"><strong>{booking['name']}</strong> 您好,行前提醒:</p>
|
| 70 |
<div style="background:#2a2a2a; padding:15px; margin:20px 0; border-left:4px solid #f1c40f;">
|
| 71 |
<ul style="color:#ddd;">
|
| 72 |
<li>📅 日期:{booking['date']}</li>
|
|
|
|
| 80 |
</div>
|
| 81 |
"""
|
| 82 |
else:
|
|
|
|
| 83 |
action_label = "確認"
|
| 84 |
mail_subject = f"訂位確認: {booking['date']} - Cié Cié Taipei"
|
| 85 |
line_text = (
|
|
|
|
| 134 |
|
| 135 |
except Exception as e: return f"🔥 嚴重錯誤: {str(e)}"
|
| 136 |
|
| 137 |
+
# 🔥🔥🔥 卡片渲染 (修正:不使用 onclick,改用 data-id) 🔥🔥🔥
|
| 138 |
def render_booking_cards():
|
| 139 |
df = get_bookings()
|
| 140 |
count_html = f"<div style='color:#bbb; margin-bottom:20px; text-align:right; font-size:16px; padding: 0 10px;'>📊 共找到 <span style='color:#fff; font-weight:bold;'>{len(df)}</span> 筆資料</div>"
|
|
|
|
| 145 |
for index, row in df.iterrows():
|
| 146 |
status = row.get('status', '待處理')
|
| 147 |
|
|
|
|
| 148 |
status_color = "#ccc"; border_color = "#444"
|
| 149 |
if '確認' in status: status_color = "#2ecc71"; border_color = "#2ecc71"
|
| 150 |
elif '取消' in status: status_color = "#e74c3c"; border_color = "#e74c3c"
|
|
|
|
| 153 |
is_canceled = '取消' in status
|
| 154 |
is_confirmed = '確認' in status
|
| 155 |
|
| 156 |
+
# 🟢 修正:移除 onclick,改用 class 和 data-id 屬性
|
| 157 |
+
# 這樣就不會依賴全域函式是否存在,而是靠下方的監聽器
|
| 158 |
+
btn_attrs = f"class='op-btn' data-id='{row['id']}'" if not is_canceled else "disabled"
|
| 159 |
|
|
|
|
| 160 |
if is_canceled:
|
| 161 |
btn_style = "background: #333; color: #666; cursor: not-allowed;"
|
| 162 |
btn_text = "🚫 已取消"
|
|
|
|
| 221 |
<div style="font-size: 1.1em; color: #000; font-weight: 900; background: #e0e0e0; padding: 8px 12px; border-radius: 6px; font-family: monospace;">
|
| 222 |
ID: {row['id']}
|
| 223 |
</div>
|
| 224 |
+
|
| 225 |
+
<button {btn_attrs} style="
|
| 226 |
+
border: none; padding: 14px 30px; border-radius: 8px; font-size: 1.1em;
|
| 227 |
+
transition: all 0.2s; min-width: 150px; cursor: pointer; {btn_style}">
|
| 228 |
{btn_text}
|
| 229 |
</button>
|
| 230 |
</div>
|
|
|
|
| 245 |
}
|
| 246 |
else: return {error_msg: "<span style='color: red'>❌ 帳號或密碼錯誤</span>"}
|
| 247 |
|
| 248 |
+
# 🔥🔥🔥 絕對有效的事件監聽器 (Event Delegation) 🔥🔥🔥
|
| 249 |
+
# 這次不是定義函式,而是直接監聽整個文件的點擊事件。
|
| 250 |
+
# 只要點擊了 class="op-btn" 的元素,就執行。這不依賴作用域,一定有效。
|
| 251 |
+
GLOBAL_JS = """
|
| 252 |
+
<script>
|
| 253 |
+
document.addEventListener('DOMContentLoaded', function() {
|
| 254 |
+
console.log("🟢 訂位系統監聽器已啟動");
|
| 255 |
+
});
|
| 256 |
+
|
| 257 |
+
// 全域監聽點擊 (Event Delegation)
|
| 258 |
+
document.addEventListener('click', function(e) {
|
| 259 |
+
// 檢查點擊的目標是否是我們的操作按鈕
|
| 260 |
+
if (e.target && e.target.classList.contains('op-btn')) {
|
| 261 |
+
const id = e.target.getAttribute('data-id');
|
| 262 |
+
console.log("👉 偵測到按鈕點擊,ID:", id);
|
| 263 |
+
|
| 264 |
+
if (!id) return;
|
| 265 |
+
|
| 266 |
+
// 1. 找輸入框
|
| 267 |
+
const idInput = document.querySelector('#hidden_id_input input');
|
| 268 |
+
|
| 269 |
+
if (idInput) {
|
| 270 |
+
console.log("✅ 寫入 ID 到 Gradio...");
|
| 271 |
+
idInput.value = id;
|
| 272 |
+
idInput.dispatchEvent(new Event('input', { bubbles: true }));
|
| 273 |
+
|
| 274 |
+
// 2. 觸發發送按鈕
|
| 275 |
+
setTimeout(() => {
|
| 276 |
+
const sendBtn = document.querySelector('#hidden_send_btn');
|
| 277 |
+
if (sendBtn) {
|
| 278 |
+
console.log("🚀 觸發 Python 發送邏輯...");
|
| 279 |
+
sendBtn.click();
|
| 280 |
+
} else {
|
| 281 |
+
console.error("❌ 找不到 hidden_send_btn");
|
| 282 |
+
}
|
| 283 |
+
}, 100);
|
| 284 |
+
} else {
|
| 285 |
+
console.error("❌ 找不到 hidden_id_input");
|
| 286 |
+
alert("系統錯誤:找不到對應的輸入框,請重新整理頁面。");
|
| 287 |
+
}
|
| 288 |
+
}
|
| 289 |
+
});
|
| 290 |
+
</script>
|
| 291 |
+
"""
|
| 292 |
|
| 293 |
+
# --- CSS (確保按鈕可按) ---
|
| 294 |
custom_css = """
|
| 295 |
body, .gradio-container { background-color: #0F0F0F; color: #fff; }
|
| 296 |
#booking_display { height: auto !important; max-height: none !important; overflow: visible !important; margin-bottom: 50px; }
|
| 297 |
button:active { transform: scale(0.96); }
|
| 298 |
#header-panel { background: #1a1a1a; padding: 15px; margin-bottom: 20px; border-radius: 10px; }
|
| 299 |
|
| 300 |
+
/* 讓操作按鈕有點擊指標 */
|
| 301 |
+
.op-btn { pointer-events: auto !important; }
|
| 302 |
+
|
| 303 |
+
/* 隱藏操作區:移出畫面外 */
|
| 304 |
#hidden_ops {
|
| 305 |
position: absolute !important;
|
| 306 |
left: -9999px !important;
|
|
|
|
| 311 |
}
|
| 312 |
"""
|
| 313 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 314 |
# --- 介面 ---
|
| 315 |
with gr.Blocks(title="Admin") as demo:
|
| 316 |
|
| 317 |
+
# 注入 JS (直接執行,不包在 function 裡)
|
| 318 |
gr.HTML(GLOBAL_JS)
|
| 319 |
|
| 320 |
with gr.Group(visible=True) as login_row:
|
|
|
|
| 332 |
|
| 333 |
booking_display = gr.HTML(elem_id="booking_display")
|
| 334 |
|
| 335 |
+
# 隱藏操作區
|
| 336 |
with gr.Column(visible=True, elem_id="hidden_ops"):
|
| 337 |
hidden_id_input = gr.Number(elem_id="hidden_id_input", precision=0)
|
| 338 |
hidden_send_btn = gr.Button("Send", elem_id="hidden_send_btn")
|