Initial commit: 台銀即期賣出匯率換算工具
This commit is contained in:
130
bot_rates_gui.py
Normal file
130
bot_rates_gui.py
Normal file
@@ -0,0 +1,130 @@
|
||||
import csv
|
||||
import io
|
||||
import threading
|
||||
import tkinter as tk
|
||||
import urllib.request
|
||||
from datetime import datetime
|
||||
from tkinter import ttk
|
||||
|
||||
URL = "https://rate.bot.com.tw/xrt/flcsv/0/day"
|
||||
TARGETS = [
|
||||
("USD", "美元"),
|
||||
("JPY", "日幣"),
|
||||
("EUR", "歐元"),
|
||||
]
|
||||
|
||||
|
||||
def fetch_rates():
|
||||
req = urllib.request.Request(URL, headers={"User-Agent": "Mozilla/5.0"})
|
||||
with urllib.request.urlopen(req, timeout=10) as resp:
|
||||
raw = resp.read().decode("utf-8-sig", errors="replace")
|
||||
reader = csv.reader(io.StringIO(raw))
|
||||
next(reader, None)
|
||||
rates = {}
|
||||
wanted = {code for code, _ in TARGETS}
|
||||
for row in reader:
|
||||
if not row:
|
||||
continue
|
||||
code = row[0].strip()
|
||||
if code in wanted:
|
||||
rates[code] = float(row[13])
|
||||
return rates
|
||||
|
||||
|
||||
class App(tk.Tk):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.title("台灣銀行 即期賣出匯率")
|
||||
self.geometry("360x320")
|
||||
self.configure(bg="#f5f6fa")
|
||||
self.resizable(False, False)
|
||||
|
||||
style = ttk.Style(self)
|
||||
try:
|
||||
style.theme_use("vista")
|
||||
except tk.TclError:
|
||||
style.theme_use("clam")
|
||||
|
||||
style.configure("TFrame", background="#f5f6fa")
|
||||
style.configure("Title.TLabel",
|
||||
background="#f5f6fa",
|
||||
foreground="#1f2937",
|
||||
font=("Microsoft JhengHei UI", 14, "bold"))
|
||||
style.configure("Name.TLabel",
|
||||
background="#ffffff",
|
||||
foreground="#374151",
|
||||
font=("Microsoft JhengHei UI", 12))
|
||||
style.configure("Rate.TLabel",
|
||||
background="#ffffff",
|
||||
foreground="#0f766e",
|
||||
font=("Consolas", 14, "bold"))
|
||||
style.configure("Hint.TLabel",
|
||||
background="#f5f6fa",
|
||||
foreground="#6b7280",
|
||||
font=("Microsoft JhengHei UI", 9))
|
||||
style.configure("Accent.TButton",
|
||||
font=("Microsoft JhengHei UI", 10, "bold"),
|
||||
padding=6)
|
||||
|
||||
container = ttk.Frame(self, padding=(18, 16))
|
||||
container.pack(fill="both", expand=True)
|
||||
|
||||
ttk.Label(container, text="台灣銀行 即期賣出", style="Title.TLabel").pack(anchor="w")
|
||||
ttk.Label(container, text="資料來源:rate.bot.com.tw",
|
||||
style="Hint.TLabel").pack(anchor="w", pady=(2, 12))
|
||||
|
||||
card = tk.Frame(container, bg="#ffffff", highlightthickness=1,
|
||||
highlightbackground="#e5e7eb")
|
||||
card.pack(fill="x")
|
||||
|
||||
self.rate_labels = {}
|
||||
for i, (code, name) in enumerate(TARGETS):
|
||||
row = tk.Frame(card, bg="#ffffff")
|
||||
row.pack(fill="x", padx=14, pady=10)
|
||||
ttk.Label(row, text=f"{name} {code}", style="Name.TLabel").pack(side="left")
|
||||
lbl = ttk.Label(row, text="--", style="Rate.TLabel")
|
||||
lbl.pack(side="right")
|
||||
self.rate_labels[code] = lbl
|
||||
if i < len(TARGETS) - 1:
|
||||
tk.Frame(card, bg="#f1f2f6", height=1).pack(fill="x", padx=10)
|
||||
|
||||
bottom = ttk.Frame(container)
|
||||
bottom.pack(fill="x", pady=(14, 0))
|
||||
|
||||
self.status = ttk.Label(bottom, text="載入中…", style="Hint.TLabel")
|
||||
self.status.pack(side="left")
|
||||
|
||||
self.btn = ttk.Button(bottom, text="重新整理", style="Accent.TButton",
|
||||
command=self.refresh)
|
||||
self.btn.pack(side="right")
|
||||
|
||||
self.after(100, self.refresh)
|
||||
|
||||
def refresh(self):
|
||||
self.btn.state(["disabled"])
|
||||
self.status.configure(text="載入中…", foreground="#6b7280")
|
||||
threading.Thread(target=self._worker, daemon=True).start()
|
||||
|
||||
def _worker(self):
|
||||
try:
|
||||
rates = fetch_rates()
|
||||
self.after(0, self._on_success, rates)
|
||||
except Exception as e:
|
||||
self.after(0, self._on_error, e)
|
||||
|
||||
def _on_success(self, rates):
|
||||
for code, _ in TARGETS:
|
||||
value = rates.get(code)
|
||||
text = f"{value:.4f}" if value is not None else "查無資料"
|
||||
self.rate_labels[code].configure(text=text)
|
||||
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
self.status.configure(text=f"更新時間:{now}", foreground="#6b7280")
|
||||
self.btn.state(["!disabled"])
|
||||
|
||||
def _on_error(self, err):
|
||||
self.status.configure(text=f"載入失敗:{err}", foreground="#b91c1c")
|
||||
self.btn.state(["!disabled"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
App().mainloop()
|
||||
Reference in New Issue
Block a user