yangtb24 commited on
Commit
eaf2f47
·
verified ·
1 Parent(s): c903c67

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +82 -40
  2. 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
- scheduler = BackgroundScheduler(daemon=True)
 
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
- print(f"[{datetime.now()}] 账户 {email} 获取 Profile 成功。")
 
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
- return
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"] = card_error
237
- print(f"[{datetime.now()}] 账户 {email} 获取卡片信息失败: {card_error}")
238
- else:
239
- accounts_data[email]["cards_info"] = None
240
- accounts_data[email]["last_card_info_success"] = True
241
- accounts_data[email]["card_info_error"] = None
242
- print(f"[{datetime.now()}] 账户 {email} 获取卡片信息成功,但无卡片数据。")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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: #eaeaea;
16
  --card-bg-color: #ffffff;
17
  --accent-color: #000000;
18
  --error-color: #ff0000;
19
- --success-color: #0070f3;
20
- --summary-bar-bg: #f9f9f9;
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 2px 4px rgba(0,0,0,0.03), 0 1px 2px rgba(0,0,0,0.03);
76
  }
77
  .summary-item {
78
  text-align: center;
@@ -102,7 +102,7 @@
102
  display: flex;
103
  }
104
  .actions button {
105
- padding: 10px 20px;
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: opacity 0.2s ease;
115
  }
116
- .actions button:hover {
117
- opacity: 0.8;
 
118
  }
119
- .actions button#logout-btn {
120
- background-color: var(--bg-color);
121
- color: var(--accent-color);
122
  border: 1px solid var(--border-color);
123
  }
124
- .actions button#logout-btn:hover {
125
- background-color: #f9f9f9;
126
- border-color: #dddddd;
 
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 2px 4px rgba(0,0,0,0.03), 0 1px 2px rgba(0,0,0,0.03);
141
  transition: box-shadow 0.2s ease, transform 0.2s ease;
142
  }
143
  .account-card:hover {
144
- box-shadow: 0 5px 15px rgba(0,0,0,0.05), 0 3px 6px rgba(0,0,0,0.05);
145
- transform: translateY(-2px);
146
  }
147
 
148
  .account-info h3 {
@@ -267,7 +269,8 @@
267
  function formatDateTime(isoString) {
268
  if (!isoString) return 'N/A';
269
  try {
270
- return new Date(isoString).toLocaleString('zh-CN', { hour12: false });
 
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';