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';
|