Upload 2 files
Browse files- app.py +82 -40
- templates/dashboard.html +27 -24
app.py
CHANGED
@@ -1,10 +1,11 @@
|
|
1 |
import os
|
2 |
import json
|
3 |
import threading
|
4 |
-
from datetime import datetime
|
5 |
from flask import Flask, request, jsonify, render_template_string, redirect, url_for, session
|
6 |
import requests
|
7 |
from apscheduler.schedulers.background import BackgroundScheduler
|
|
|
8 |
from dotenv import load_dotenv
|
9 |
|
10 |
load_dotenv()
|
@@ -14,6 +15,7 @@ app.secret_key = os.getenv("SECRET_KEY")
|
|
14 |
if not app.secret_key:
|
15 |
print("警告: SECRET_KEY 环境变量未设置。将使用默认的、不安全的密钥。请在生产环境中设置一个安全的 SECRET_KEY。")
|
16 |
app.secret_key = "dev_secret_key_for_testing_only_change_me"
|
|
|
17 |
|
18 |
LOGIN_URL = "https://api-card.infini.money/user/login"
|
19 |
PROFILE_URL = "https://api-card.infini.money/user/profile"
|
@@ -22,7 +24,8 @@ FRONTEND_PASSWORD = os.getenv("PASSWORD")
|
|
22 |
ACCOUNTS_JSON = os.getenv("ACCOUNTS")
|
23 |
|
24 |
accounts_data = {}
|
25 |
-
|
|
|
26 |
data_lock = threading.Lock()
|
27 |
|
28 |
def parse_accounts():
|
@@ -116,7 +119,7 @@ def get_api_card_info(email, token):
|
|
116 |
return None, "Token 为空,无法获取卡片信息。"
|
117 |
|
118 |
cookies = {"jwt_token": token}
|
119 |
-
print(f"[{datetime.now()}] 尝试为账户 {email} 获取卡片信息...")
|
120 |
try:
|
121 |
response = requests.get(CARD_INFO_URL, cookies=cookies, timeout=10)
|
122 |
response.raise_for_status()
|
@@ -165,22 +168,22 @@ def login_and_store_token(email):
|
|
165 |
return
|
166 |
|
167 |
password = account_info["password"]
|
168 |
-
print(f"[{datetime.now()}] 尝试为账户 {email} 登录...")
|
169 |
|
170 |
token, error = api_login(email, password)
|
171 |
|
172 |
with data_lock:
|
173 |
-
accounts_data[email]["last_login_attempt"] = datetime.now()
|
174 |
if token:
|
175 |
accounts_data[email]["token"] = token
|
176 |
accounts_data[email]["last_login_success"] = True
|
177 |
accounts_data[email]["login_error"] = None
|
178 |
-
print(f"[{datetime.now()}] 账户 {email} 登录成功。")
|
179 |
else:
|
180 |
accounts_data[email]["token"] = None
|
181 |
accounts_data[email]["last_login_success"] = False
|
182 |
accounts_data[email]["login_error"] = error
|
183 |
-
print(f"[{datetime.now()}] 账户 {email} 登录失败: {error}")
|
184 |
|
185 |
def fetch_and_store_profile(email):
|
186 |
global accounts_data
|
@@ -191,55 +194,93 @@ def fetch_and_store_profile(email):
|
|
191 |
return
|
192 |
token = account_info.get("token")
|
193 |
|
|
|
194 |
if not token:
|
195 |
-
print(f"[{datetime.now()}] 账户 {email} 没有有效的 token,跳过获取 Profile。")
|
196 |
with data_lock:
|
197 |
-
accounts_data[email]["last_profile_attempt"] = datetime.now()
|
198 |
accounts_data[email]["last_profile_success"] = False
|
199 |
accounts_data[email]["profile_error"] = "无有效 Token"
|
200 |
accounts_data[email]["profile"] = None
|
|
|
|
|
|
|
|
|
|
|
201 |
return
|
202 |
|
203 |
-
print(f"[{datetime.now()}] 尝试为账户 {email} 获取 Profile...")
|
204 |
profile, error = get_api_profile(email, token)
|
205 |
|
|
|
206 |
with data_lock:
|
207 |
-
accounts_data[email]["last_profile_attempt"] = datetime.now()
|
208 |
if profile:
|
209 |
accounts_data[email]["profile"] = profile
|
210 |
accounts_data[email]["last_profile_success"] = True
|
211 |
accounts_data[email]["profile_error"] = None
|
212 |
-
|
|
|
213 |
else:
|
214 |
accounts_data[email]["profile"] = None
|
215 |
accounts_data[email]["last_profile_success"] = False
|
216 |
accounts_data[email]["profile_error"] = error
|
217 |
-
print(f"[{datetime.now()}] 账户 {email} 获取 Profile 失败: {error}")
|
218 |
if error and ("token" in error.lower() or "auth" in error.lower() or "登录" in error.lower()):
|
219 |
-
print(f"[{datetime.now()}] 账户 {email} 获取 Profile 失败,疑似 Token 失效,将尝试重新登录。")
|
220 |
-
accounts_data[email]["token"] = None
|
221 |
-
|
222 |
-
|
223 |
-
print(f"[{datetime.now()}] Profile 获取成功,继续为账户 {email} 获取卡片信息...")
|
224 |
-
cards_info, card_error = get_api_card_info(email, token)
|
225 |
-
|
226 |
-
with data_lock:
|
227 |
-
accounts_data[email]["last_card_info_attempt"] = datetime.now()
|
228 |
-
if cards_info:
|
229 |
-
accounts_data[email]["cards_info"] = cards_info
|
230 |
-
accounts_data[email]["last_card_info_success"] = True
|
231 |
-
accounts_data[email]["card_info_error"] = None
|
232 |
-
print(f"[{datetime.now()}] 账户 {email} 获取卡片信息成功。")
|
233 |
-
elif card_error:
|
234 |
accounts_data[email]["cards_info"] = None
|
235 |
accounts_data[email]["last_card_info_success"] = False
|
236 |
-
accounts_data[email]["card_info_error"] =
|
237 |
-
|
238 |
-
|
239 |
-
|
240 |
-
|
241 |
-
|
242 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
243 |
|
244 |
def initial_login_all_accounts():
|
245 |
print("程序启动,开始为所有账户执行初始登录...")
|
@@ -256,7 +297,7 @@ def initial_login_all_accounts():
|
|
256 |
print("所有账户初始登录尝试完成。")
|
257 |
|
258 |
def scheduled_login_all_accounts():
|
259 |
-
print(f"[{datetime.now()}] 定时任务:开始为所有账户重新登录...")
|
260 |
threads = []
|
261 |
with data_lock:
|
262 |
emails_to_login = list(accounts_data.keys())
|
@@ -267,11 +308,11 @@ def scheduled_login_all_accounts():
|
|
267 |
thread.start()
|
268 |
for thread in threads:
|
269 |
thread.join()
|
270 |
-
print(f"[{datetime.now()}] 定时任务:所有账户重新登录尝试完成。")
|
271 |
scheduled_fetch_all_profiles()
|
272 |
|
273 |
def scheduled_fetch_all_profiles():
|
274 |
-
print(f"[{datetime.now()}] 定时任务:开始为所有账户获取 Profile...")
|
275 |
threads = []
|
276 |
with data_lock:
|
277 |
emails_to_fetch = list(accounts_data.keys())
|
@@ -282,7 +323,7 @@ def scheduled_fetch_all_profiles():
|
|
282 |
thread.start()
|
283 |
for thread in threads:
|
284 |
thread.join()
|
285 |
-
print(f"[{datetime.now()}] 定时任务:所有账户获取 Profile 尝试完成。")
|
286 |
|
287 |
LOGIN_FORM_HTML = """
|
288 |
<!DOCTYPE html>
|
@@ -391,6 +432,7 @@ def login_frontend():
|
|
391 |
entered_password = request.form.get('password')
|
392 |
if entered_password == FRONTEND_PASSWORD:
|
393 |
session['logged_in'] = True
|
|
|
394 |
return redirect(url_for('dashboard'))
|
395 |
else:
|
396 |
error = "密码错误!"
|
@@ -436,7 +478,7 @@ def manual_refresh_all_data():
|
|
436 |
if not ('logged_in' in session and session['logged_in']):
|
437 |
return jsonify({"error": "未授权访问"}), 401
|
438 |
|
439 |
-
print(f"[{datetime.now()}] 手动触发数据刷新...")
|
440 |
threading.Thread(target=scheduled_login_all_accounts).start()
|
441 |
return jsonify({"message": "刷新任务已启动,请稍后查看数据。"}), 202
|
442 |
|
|
|
1 |
import os
|
2 |
import json
|
3 |
import threading
|
4 |
+
from datetime import datetime, timezone, timedelta
|
5 |
from flask import Flask, request, jsonify, render_template_string, redirect, url_for, session
|
6 |
import requests
|
7 |
from apscheduler.schedulers.background import BackgroundScheduler
|
8 |
+
import pytz
|
9 |
from dotenv import load_dotenv
|
10 |
|
11 |
load_dotenv()
|
|
|
15 |
if not app.secret_key:
|
16 |
print("警告: SECRET_KEY 环境变量未设置。将使用默认的、不安全的密钥。请在生产环境中设置一个安全的 SECRET_KEY。")
|
17 |
app.secret_key = "dev_secret_key_for_testing_only_change_me"
|
18 |
+
app.permanent_session_lifetime = timedelta(days=30)
|
19 |
|
20 |
LOGIN_URL = "https://api-card.infini.money/user/login"
|
21 |
PROFILE_URL = "https://api-card.infini.money/user/profile"
|
|
|
24 |
ACCOUNTS_JSON = os.getenv("ACCOUNTS")
|
25 |
|
26 |
accounts_data = {}
|
27 |
+
shanghai_tz = pytz.timezone('Asia/Shanghai')
|
28 |
+
scheduler = BackgroundScheduler(daemon=True, timezone=shanghai_tz)
|
29 |
data_lock = threading.Lock()
|
30 |
|
31 |
def parse_accounts():
|
|
|
119 |
return None, "Token 为空,无法获取卡片信息。"
|
120 |
|
121 |
cookies = {"jwt_token": token}
|
122 |
+
print(f"[{datetime.now(shanghai_tz)}] 尝试为账户 {email} 获取卡片信息...")
|
123 |
try:
|
124 |
response = requests.get(CARD_INFO_URL, cookies=cookies, timeout=10)
|
125 |
response.raise_for_status()
|
|
|
168 |
return
|
169 |
|
170 |
password = account_info["password"]
|
171 |
+
print(f"[{datetime.now(shanghai_tz)}] 尝试为账户 {email} 登录...")
|
172 |
|
173 |
token, error = api_login(email, password)
|
174 |
|
175 |
with data_lock:
|
176 |
+
accounts_data[email]["last_login_attempt"] = datetime.now(shanghai_tz)
|
177 |
if token:
|
178 |
accounts_data[email]["token"] = token
|
179 |
accounts_data[email]["last_login_success"] = True
|
180 |
accounts_data[email]["login_error"] = None
|
181 |
+
print(f"[{datetime.now(shanghai_tz)}] 账户 {email} 登录成功。")
|
182 |
else:
|
183 |
accounts_data[email]["token"] = None
|
184 |
accounts_data[email]["last_login_success"] = False
|
185 |
accounts_data[email]["login_error"] = error
|
186 |
+
print(f"[{datetime.now(shanghai_tz)}] 账户 {email} 登录失败: {error}")
|
187 |
|
188 |
def fetch_and_store_profile(email):
|
189 |
global accounts_data
|
|
|
194 |
return
|
195 |
token = account_info.get("token")
|
196 |
|
197 |
+
# 以下所有逻辑都应在 fetch_and_store_profile 函数内部
|
198 |
if not token:
|
199 |
+
print(f"[{datetime.now(shanghai_tz)}] 账户 {email} 没有有效的 token,跳过获取 Profile。")
|
200 |
with data_lock:
|
201 |
+
accounts_data[email]["last_profile_attempt"] = datetime.now(shanghai_tz)
|
202 |
accounts_data[email]["last_profile_success"] = False
|
203 |
accounts_data[email]["profile_error"] = "无有效 Token"
|
204 |
accounts_data[email]["profile"] = None
|
205 |
+
# 由于没有token,卡片信息也无法获取
|
206 |
+
accounts_data[email]["last_card_info_attempt"] = datetime.now(shanghai_tz)
|
207 |
+
accounts_data[email]["cards_info"] = None
|
208 |
+
accounts_data[email]["last_card_info_success"] = False
|
209 |
+
accounts_data[email]["card_info_error"] = "因 Token 为空未尝试"
|
210 |
return
|
211 |
|
212 |
+
print(f"[{datetime.now(shanghai_tz)}] 尝试为账户 {email} 获取 Profile...")
|
213 |
profile, error = get_api_profile(email, token)
|
214 |
|
215 |
+
profile_fetch_successful_this_attempt = False
|
216 |
with data_lock:
|
217 |
+
accounts_data[email]["last_profile_attempt"] = datetime.now(shanghai_tz)
|
218 |
if profile:
|
219 |
accounts_data[email]["profile"] = profile
|
220 |
accounts_data[email]["last_profile_success"] = True
|
221 |
accounts_data[email]["profile_error"] = None
|
222 |
+
profile_fetch_successful_this_attempt = True
|
223 |
+
print(f"[{datetime.now(shanghai_tz)}] 账户 {email} 获取 Profile 成功。")
|
224 |
else:
|
225 |
accounts_data[email]["profile"] = None
|
226 |
accounts_data[email]["last_profile_success"] = False
|
227 |
accounts_data[email]["profile_error"] = error
|
228 |
+
print(f"[{datetime.now(shanghai_tz)}] 账户 {email} 获取 Profile 失败: {error}")
|
229 |
if error and ("token" in error.lower() or "auth" in error.lower() or "登录" in error.lower()):
|
230 |
+
print(f"[{datetime.now(shanghai_tz)}] 账户 {email} 获取 Profile 失败,疑似 Token 失效,将尝试重新登录。")
|
231 |
+
accounts_data[email]["token"] = None # 清除失效的token
|
232 |
+
# 如果获取 profile 失败,则不应继续获取卡片信息
|
233 |
+
accounts_data[email]["last_card_info_attempt"] = datetime.now(shanghai_tz)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
234 |
accounts_data[email]["cards_info"] = None
|
235 |
accounts_data[email]["last_card_info_success"] = False
|
236 |
+
accounts_data[email]["card_info_error"] = "因 Profile 获取失败未尝试"
|
237 |
+
return # 确保在这里返回,不再执行后续的卡片信息获取
|
238 |
+
|
239 |
+
# 只有当 profile 获取成功时才继续获取卡片信息
|
240 |
+
if profile_fetch_successful_this_attempt:
|
241 |
+
# 重新从 data_lock 内获取 token,因为它可能在上面被清除了
|
242 |
+
current_token_for_cards = None
|
243 |
+
with data_lock:
|
244 |
+
# 确保在访问 token 前,account_info 仍然有效且 email 存在
|
245 |
+
if email in accounts_data:
|
246 |
+
current_token_for_cards = accounts_data[email].get("token")
|
247 |
+
else: # 理论上不应该发生,但作为防御性编程
|
248 |
+
print(f"[{datetime.now(shanghai_tz)}] 账户 {email} 在获取卡片信息前数据结构异常,跳过。")
|
249 |
+
return
|
250 |
+
|
251 |
+
|
252 |
+
if not current_token_for_cards:
|
253 |
+
print(f"[{datetime.now(shanghai_tz)}] 账户 {email} 在获取卡片信息前发现 Token 已失效或被清除,跳过。")
|
254 |
+
with data_lock:
|
255 |
+
if email in accounts_data: # 再次检查
|
256 |
+
accounts_data[email]["last_card_info_attempt"] = datetime.now(shanghai_tz)
|
257 |
+
accounts_data[email]["cards_info"] = None
|
258 |
+
accounts_data[email]["last_card_info_success"] = False
|
259 |
+
accounts_data[email]["card_info_error"] = "因 Token 失效未尝试"
|
260 |
+
return
|
261 |
+
|
262 |
+
print(f"[{datetime.now(shanghai_tz)}] Profile 获取成功,继续为账户 {email} 获取卡片信息...")
|
263 |
+
cards_info, card_error = get_api_card_info(email, current_token_for_cards)
|
264 |
+
|
265 |
+
with data_lock:
|
266 |
+
if email in accounts_data: # 再次检查
|
267 |
+
accounts_data[email]["last_card_info_attempt"] = datetime.now(shanghai_tz)
|
268 |
+
if cards_info:
|
269 |
+
accounts_data[email]["cards_info"] = cards_info
|
270 |
+
accounts_data[email]["last_card_info_success"] = True
|
271 |
+
accounts_data[email]["card_info_error"] = None
|
272 |
+
print(f"[{datetime.now(shanghai_tz)}] 账户 {email} 获取卡片信息成功。")
|
273 |
+
elif card_error: # API 调用有错误
|
274 |
+
accounts_data[email]["cards_info"] = None
|
275 |
+
accounts_data[email]["last_card_info_success"] = False
|
276 |
+
accounts_data[email]["card_info_error"] = card_error
|
277 |
+
print(f"[{datetime.now(shanghai_tz)}] 账户 {email} 获取卡片信息失败: {card_error}")
|
278 |
+
else: # API 调用成功,但没有卡片数据
|
279 |
+
accounts_data[email]["cards_info"] = None
|
280 |
+
accounts_data[email]["last_card_info_success"] = True # 标记为成功,因为API��用是成功的
|
281 |
+
accounts_data[email]["card_info_error"] = None # 没有错误
|
282 |
+
print(f"[{datetime.now(shanghai_tz)}] 账户 {email} 获取卡片信息成功,但无卡片数据。")
|
283 |
+
# else 分支(profile_fetch_successful_this_attempt 为 False)已在上面处理 profile 获取失败的情况并返回
|
284 |
|
285 |
def initial_login_all_accounts():
|
286 |
print("程序启动,开始为所有账户执行初始登录...")
|
|
|
297 |
print("所有账户初始登录尝试完成。")
|
298 |
|
299 |
def scheduled_login_all_accounts():
|
300 |
+
print(f"[{datetime.now(shanghai_tz)}] 定时任务:开始为所有账户重新登录...")
|
301 |
threads = []
|
302 |
with data_lock:
|
303 |
emails_to_login = list(accounts_data.keys())
|
|
|
308 |
thread.start()
|
309 |
for thread in threads:
|
310 |
thread.join()
|
311 |
+
print(f"[{datetime.now(shanghai_tz)}] 定时任务:所有账户重新登录尝试完成。")
|
312 |
scheduled_fetch_all_profiles()
|
313 |
|
314 |
def scheduled_fetch_all_profiles():
|
315 |
+
print(f"[{datetime.now(shanghai_tz)}] 定时任务:开始为所有账户获取 Profile...")
|
316 |
threads = []
|
317 |
with data_lock:
|
318 |
emails_to_fetch = list(accounts_data.keys())
|
|
|
323 |
thread.start()
|
324 |
for thread in threads:
|
325 |
thread.join()
|
326 |
+
print(f"[{datetime.now(shanghai_tz)}] 定时任务:所有账户获取 Profile 尝试完成。")
|
327 |
|
328 |
LOGIN_FORM_HTML = """
|
329 |
<!DOCTYPE html>
|
|
|
432 |
entered_password = request.form.get('password')
|
433 |
if entered_password == FRONTEND_PASSWORD:
|
434 |
session['logged_in'] = True
|
435 |
+
session.permanent = True
|
436 |
return redirect(url_for('dashboard'))
|
437 |
else:
|
438 |
error = "密码错误!"
|
|
|
478 |
if not ('logged_in' in session and session['logged_in']):
|
479 |
return jsonify({"error": "未授权访问"}), 401
|
480 |
|
481 |
+
print(f"[{datetime.now(shanghai_tz)}] 手动触发数据刷新...")
|
482 |
threading.Thread(target=scheduled_login_all_accounts).start()
|
483 |
return jsonify({"message": "刷新任务已启动,请稍后查看数据。"}), 202
|
484 |
|
templates/dashboard.html
CHANGED
@@ -4,20 +4,20 @@
|
|
4 |
<meta charset="UTF-8">
|
5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6 |
<title>账户仪表盘</title>
|
7 |
-
<link rel="preconnect" href="https
|
8 |
-
<link rel="preconnect" href="https
|
9 |
-
<link href="https:
|
10 |
<style>
|
11 |
:root {
|
12 |
--bg-color: #ffffff;
|
13 |
--text-color: #000000;
|
14 |
--secondary-text-color: #666666;
|
15 |
-
--border-color: #
|
16 |
--card-bg-color: #ffffff;
|
17 |
--accent-color: #000000;
|
18 |
--error-color: #ff0000;
|
19 |
-
--success-color: #
|
20 |
-
--summary-bar-bg: #
|
21 |
}
|
22 |
|
23 |
body {
|
@@ -72,7 +72,7 @@
|
|
72 |
border-radius: 8px;
|
73 |
margin-bottom: 30px;
|
74 |
border: 1px solid var(--border-color);
|
75 |
-
box-shadow: 0
|
76 |
}
|
77 |
.summary-item {
|
78 |
text-align: center;
|
@@ -102,7 +102,7 @@
|
|
102 |
display: flex;
|
103 |
}
|
104 |
.actions button {
|
105 |
-
padding:
|
106 |
background-color: var(--accent-color);
|
107 |
color: var(--bg-color);
|
108 |
border: 1px solid var(--accent-color);
|
@@ -111,21 +111,23 @@
|
|
111 |
font-size: 14px;
|
112 |
font-weight: 500;
|
113 |
margin-left: 12px;
|
114 |
-
transition:
|
115 |
}
|
116 |
-
.actions button:hover {
|
117 |
-
|
|
|
118 |
}
|
119 |
-
.actions button#logout-btn {
|
120 |
-
background-color: var(--bg-color);
|
121 |
-
color: var(--
|
122 |
border: 1px solid var(--border-color);
|
123 |
}
|
124 |
-
.actions button#logout-btn:hover {
|
125 |
-
background-color: #
|
126 |
-
border-color: #
|
|
|
127 |
}
|
128 |
-
|
129 |
#account-cards-container {
|
130 |
display: grid;
|
131 |
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
@@ -137,12 +139,12 @@
|
|
137 |
background-color: var(--card-bg-color);
|
138 |
border-radius: 8px;
|
139 |
border: 1px solid var(--border-color);
|
140 |
-
box-shadow: 0
|
141 |
transition: box-shadow 0.2s ease, transform 0.2s ease;
|
142 |
}
|
143 |
.account-card:hover {
|
144 |
-
box-shadow: 0
|
145 |
-
transform: translateY(-
|
146 |
}
|
147 |
|
148 |
.account-info h3 {
|
@@ -267,7 +269,8 @@
|
|
267 |
function formatDateTime(isoString) {
|
268 |
if (!isoString) return 'N/A';
|
269 |
try {
|
270 |
-
|
|
|
271 |
} catch (e) {
|
272 |
return 'Invalid Date';
|
273 |
}
|
@@ -279,7 +282,7 @@
|
|
279 |
|
280 |
const numAccounts = Object.keys(data).length;
|
281 |
totalAccountsValueElem.textContent = numAccounts;
|
282 |
-
const currentTime = new Date().toLocaleString('zh-CN', { hour12: false });
|
283 |
lastUpdatedElem.textContent = `最后更新时间: ${currentTime}`;
|
284 |
|
285 |
|
@@ -437,7 +440,7 @@
|
|
437 |
|
438 |
} catch (error) {
|
439 |
console.error('Error triggering refresh:', error);
|
440 |
-
lastUpdatedElem.textContent = `手动刷新失败: ${new Date().toLocaleString('zh-CN', { hour12: false })}`;
|
441 |
refreshButton.textContent = originalButtonText;
|
442 |
refreshButton.disabled = false;
|
443 |
loadingIndicator.style.display = 'none';
|
|
|
4 |
<meta charset="UTF-8">
|
5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6 |
<title>账户仪表盘</title>
|
7 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
8 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
9 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
10 |
<style>
|
11 |
:root {
|
12 |
--bg-color: #ffffff;
|
13 |
--text-color: #000000;
|
14 |
--secondary-text-color: #666666;
|
15 |
+
--border-color: #e0e0e0;
|
16 |
--card-bg-color: #ffffff;
|
17 |
--accent-color: #000000;
|
18 |
--error-color: #ff0000;
|
19 |
+
--success-color: #000000;
|
20 |
+
--summary-bar-bg: #fafafa;
|
21 |
}
|
22 |
|
23 |
body {
|
|
|
72 |
border-radius: 8px;
|
73 |
margin-bottom: 30px;
|
74 |
border: 1px solid var(--border-color);
|
75 |
+
box-shadow: 0 1px 3px rgba(0,0,0,0.04), 0 1px 2px rgba(0,0,0,0.06);
|
76 |
}
|
77 |
.summary-item {
|
78 |
text-align: center;
|
|
|
102 |
display: flex;
|
103 |
}
|
104 |
.actions button {
|
105 |
+
padding: 9px 18px;
|
106 |
background-color: var(--accent-color);
|
107 |
color: var(--bg-color);
|
108 |
border: 1px solid var(--accent-color);
|
|
|
111 |
font-size: 14px;
|
112 |
font-weight: 500;
|
113 |
margin-left: 12px;
|
114 |
+
transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease;
|
115 |
}
|
116 |
+
.actions button:hover {
|
117 |
+
background-color: #333333;
|
118 |
+
border-color: #333333;
|
119 |
}
|
120 |
+
.actions button#logout-btn {
|
121 |
+
background-color: var(--bg-color);
|
122 |
+
color: var(--secondary-text-color);
|
123 |
border: 1px solid var(--border-color);
|
124 |
}
|
125 |
+
.actions button#logout-btn:hover {
|
126 |
+
background-color: #f7f7f7;
|
127 |
+
border-color: #cccccc;
|
128 |
+
color: var(--text-color);
|
129 |
}
|
130 |
+
|
131 |
#account-cards-container {
|
132 |
display: grid;
|
133 |
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
|
|
139 |
background-color: var(--card-bg-color);
|
140 |
border-radius: 8px;
|
141 |
border: 1px solid var(--border-color);
|
142 |
+
box-shadow: 0 1px 3px rgba(0,0,0,0.04), 0 1px 2px rgba(0,0,0,0.06);
|
143 |
transition: box-shadow 0.2s ease, transform 0.2s ease;
|
144 |
}
|
145 |
.account-card:hover {
|
146 |
+
box-shadow: 0 4px 12px rgba(0,0,0,0.08), 0 2px 6px rgba(0,0,0,0.08);
|
147 |
+
transform: translateY(-1px);
|
148 |
}
|
149 |
|
150 |
.account-info h3 {
|
|
|
269 |
function formatDateTime(isoString) {
|
270 |
if (!isoString) return 'N/A';
|
271 |
try {
|
272 |
+
// 强制使用 UTC+8 (Asia/Shanghai) 时区
|
273 |
+
return new Date(isoString).toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai', hour12: false });
|
274 |
} catch (e) {
|
275 |
return 'Invalid Date';
|
276 |
}
|
|
|
282 |
|
283 |
const numAccounts = Object.keys(data).length;
|
284 |
totalAccountsValueElem.textContent = numAccounts;
|
285 |
+
const currentTime = new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai', hour12: false });
|
286 |
lastUpdatedElem.textContent = `最后更新时间: ${currentTime}`;
|
287 |
|
288 |
|
|
|
440 |
|
441 |
} catch (error) {
|
442 |
console.error('Error triggering refresh:', error);
|
443 |
+
lastUpdatedElem.textContent = `手动刷新失败: ${new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai', hour12: false })}`;
|
444 |
refreshButton.textContent = originalButtonText;
|
445 |
refreshButton.disabled = false;
|
446 |
loadingIndicator.style.display = 'none';
|