ZhaoShanGeng commited on
Commit
2f3f89e
·
1 Parent(s): 127d039

feat: Token过期自动刷新 + 刷新中状态显示

Browse files
Files changed (2) hide show
  1. public/js/tokens.js +72 -3
  2. public/style.css +8 -0
public/js/tokens.js CHANGED
@@ -19,6 +19,9 @@ async function loadTokens() {
19
  }
20
  }
21
 
 
 
 
22
  function renderTokens(tokens) {
23
  cachedTokens = tokens;
24
 
@@ -38,14 +41,23 @@ function renderTokens(tokens) {
38
  return;
39
  }
40
 
 
 
 
41
  tokenList.innerHTML = tokens.map(token => {
42
  const expireTime = new Date(token.timestamp + token.expires_in * 1000);
43
  const isExpired = expireTime < new Date();
 
44
  const expireStr = expireTime.toLocaleString('zh-CN', {month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit'});
45
  const cardId = token.refresh_token.substring(0, 8);
46
 
 
 
 
 
 
47
  return `
48
- <div class="token-card ${!token.enable ? 'disabled' : ''} ${isExpired ? 'expired' : ''}">
49
  <div class="token-header">
50
  <span class="status ${token.enable ? 'enabled' : 'disabled'}">
51
  ${token.enable ? '✅ 启用' : '❌ 禁用'}
@@ -70,9 +82,9 @@ function renderTokens(tokens) {
70
  <span class="info-value sensitive-info">${token.email || '点击设置'}</span>
71
  <span class="info-edit-icon">✏️</span>
72
  </div>
73
- <div class="info-row ${isExpired ? 'expired-text' : ''}">
74
  <span class="info-label">⏰</span>
75
- <span class="info-value">${expireStr}${isExpired ? ' (已过期)' : ''}</span>
76
  </div>
77
  </div>
78
  <div class="token-quota-inline" id="quota-inline-${cardId}">
@@ -97,6 +109,63 @@ function renderTokens(tokens) {
97
  });
98
 
99
  updateSensitiveInfoDisplay();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  }
101
 
102
  function showManualModal() {
 
19
  }
20
  }
21
 
22
+ // 正在刷新的 Token 集合
23
+ const refreshingTokens = new Set();
24
+
25
  function renderTokens(tokens) {
26
  cachedTokens = tokens;
27
 
 
41
  return;
42
  }
43
 
44
+ // 收集需要自动刷新的过期 Token
45
+ const expiredTokensToRefresh = [];
46
+
47
  tokenList.innerHTML = tokens.map(token => {
48
  const expireTime = new Date(token.timestamp + token.expires_in * 1000);
49
  const isExpired = expireTime < new Date();
50
+ const isRefreshing = refreshingTokens.has(token.refresh_token);
51
  const expireStr = expireTime.toLocaleString('zh-CN', {month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit'});
52
  const cardId = token.refresh_token.substring(0, 8);
53
 
54
+ // 如果已过期且启用状态,加入待刷新列表
55
+ if (isExpired && token.enable && !isRefreshing) {
56
+ expiredTokensToRefresh.push(token.refresh_token);
57
+ }
58
+
59
  return `
60
+ <div class="token-card ${!token.enable ? 'disabled' : ''} ${isExpired ? 'expired' : ''} ${isRefreshing ? 'refreshing' : ''}" id="card-${cardId}">
61
  <div class="token-header">
62
  <span class="status ${token.enable ? 'enabled' : 'disabled'}">
63
  ${token.enable ? '✅ 启用' : '❌ 禁用'}
 
82
  <span class="info-value sensitive-info">${token.email || '点击设置'}</span>
83
  <span class="info-edit-icon">✏️</span>
84
  </div>
85
+ <div class="info-row ${isExpired ? 'expired-text' : ''}" id="expire-row-${cardId}">
86
  <span class="info-label">⏰</span>
87
+ <span class="info-value">${isRefreshing ? '🔄 刷新中...' : expireStr}${isExpired && !isRefreshing ? ' (已过期)' : ''}</span>
88
  </div>
89
  </div>
90
  <div class="token-quota-inline" id="quota-inline-${cardId}">
 
109
  });
110
 
111
  updateSensitiveInfoDisplay();
112
+
113
+ // 自动刷新过期的 Token
114
+ if (expiredTokensToRefresh.length > 0) {
115
+ expiredTokensToRefresh.forEach(refreshToken => {
116
+ autoRefreshToken(refreshToken);
117
+ });
118
+ }
119
+ }
120
+
121
+ // 自动刷新过期 Token
122
+ async function autoRefreshToken(refreshToken) {
123
+ if (refreshingTokens.has(refreshToken)) return;
124
+
125
+ refreshingTokens.add(refreshToken);
126
+ const cardId = refreshToken.substring(0, 8);
127
+
128
+ // 更新 UI 显示刷新中状态
129
+ const card = document.getElementById(`card-${cardId}`);
130
+ const expireRow = document.getElementById(`expire-row-${cardId}`);
131
+ if (card) card.classList.add('refreshing');
132
+ if (expireRow) {
133
+ const valueSpan = expireRow.querySelector('.info-value');
134
+ if (valueSpan) valueSpan.textContent = '🔄 刷新中...';
135
+ }
136
+
137
+ try {
138
+ const response = await authFetch(`/admin/tokens/${encodeURIComponent(refreshToken)}/refresh`, {
139
+ method: 'POST',
140
+ headers: { 'Authorization': `Bearer ${authToken}` }
141
+ });
142
+
143
+ const data = await response.json();
144
+ if (data.success) {
145
+ showToast('Token 已自动刷新', 'success');
146
+ // 刷新成功后重新加载列表
147
+ refreshingTokens.delete(refreshToken);
148
+ loadTokens();
149
+ } else {
150
+ showToast(`Token 刷新失败: ${data.message || '未知错误'}`, 'error');
151
+ refreshingTokens.delete(refreshToken);
152
+ // 更新 UI 显示刷新失败
153
+ if (expireRow) {
154
+ const valueSpan = expireRow.querySelector('.info-value');
155
+ if (valueSpan) valueSpan.textContent = '❌ 刷新失败';
156
+ }
157
+ }
158
+ } catch (error) {
159
+ if (error.message !== 'Unauthorized') {
160
+ showToast(`Token 刷新失败: ${error.message}`, 'error');
161
+ }
162
+ refreshingTokens.delete(refreshToken);
163
+ // 更新 UI 显示刷新失败
164
+ if (expireRow) {
165
+ const valueSpan = expireRow.querySelector('.info-value');
166
+ if (valueSpan) valueSpan.textContent = '❌ 刷新失败';
167
+ }
168
+ }
169
  }
170
 
171
  function showManualModal() {
public/style.css CHANGED
@@ -422,6 +422,14 @@ button.loading::after {
422
  }
423
  .token-card.disabled { opacity: 0.6; }
424
  .token-card.expired { border-color: var(--danger); }
 
 
 
 
 
 
 
 
425
 
426
  .token-header {
427
  display: flex;
 
422
  }
423
  .token-card.disabled { opacity: 0.6; }
424
  .token-card.expired { border-color: var(--danger); }
425
+ .token-card.refreshing {
426
+ border-color: var(--warning);
427
+ animation: pulse 1.5s ease-in-out infinite;
428
+ }
429
+ @keyframes pulse {
430
+ 0%, 100% { opacity: 1; }
431
+ 50% { opacity: 0.7; }
432
+ }
433
 
434
  .token-header {
435
  display: flex;