ZhaoShanGeng commited on
Commit
fd6aebe
·
1 Parent(s): 44af8fa

优化前端界面:刷新按钮移至过期时间旁、添加筛选功能、优化动画效果、隐藏敏感信息时不显示相关行

Browse files
public/index.html CHANGED
@@ -95,15 +95,15 @@ html.tab-settings .tab[data-tab="settings"] {
95
  <!-- 统计卡片 + 操作按钮 合并 -->
96
  <div class="top-bar">
97
  <div class="stats-inline">
98
- <div class="stat-item">
99
  <span class="stat-num" id="totalTokens">0</span>
100
  <span class="stat-text">总数</span>
101
  </div>
102
- <div class="stat-item success">
103
  <span class="stat-num" id="enabledTokens">0</span>
104
  <span class="stat-text">启用</span>
105
  </div>
106
- <div class="stat-item danger">
107
  <span class="stat-num" id="disabledTokens">0</span>
108
  <span class="stat-text">禁用</span>
109
  </div>
 
95
  <!-- 统计卡片 + 操作按钮 合并 -->
96
  <div class="top-bar">
97
  <div class="stats-inline">
98
+ <div class="stat-item clickable active" onclick="filterTokens('all')">
99
  <span class="stat-num" id="totalTokens">0</span>
100
  <span class="stat-text">总数</span>
101
  </div>
102
+ <div class="stat-item success clickable" onclick="filterTokens('enabled')">
103
  <span class="stat-num" id="enabledTokens">0</span>
104
  <span class="stat-text">启用</span>
105
  </div>
106
+ <div class="stat-item danger clickable" onclick="filterTokens('disabled')">
107
  <span class="stat-num" id="disabledTokens">0</span>
108
  <span class="stat-text">禁用</span>
109
  </div>
public/js/quota.js CHANGED
@@ -102,7 +102,8 @@ async function toggleQuotaExpand(cardId, refreshToken) {
102
 
103
  if (isHidden) {
104
  detailEl.classList.remove('hidden');
105
- toggleEl.textContent = '';
 
106
 
107
  if (!detailEl.dataset.loaded) {
108
  detailEl.innerHTML = '<div class="quota-loading-small">加载中...</div>';
@@ -110,8 +111,15 @@ async function toggleQuotaExpand(cardId, refreshToken) {
110
  detailEl.dataset.loaded = 'true';
111
  }
112
  } else {
113
- detailEl.classList.add('hidden');
114
- toggleEl.textContent = '';
 
 
 
 
 
 
 
115
  }
116
  }
117
 
 
102
 
103
  if (isHidden) {
104
  detailEl.classList.remove('hidden');
105
+ detailEl.classList.remove('collapsing');
106
+ toggleEl.classList.add('expanded');
107
 
108
  if (!detailEl.dataset.loaded) {
109
  detailEl.innerHTML = '<div class="quota-loading-small">加载中...</div>';
 
111
  detailEl.dataset.loaded = 'true';
112
  }
113
  } else {
114
+ // 添加收起动画
115
+ detailEl.classList.add('collapsing');
116
+ toggleEl.classList.remove('expanded');
117
+
118
+ // 动画结束后隐藏
119
+ setTimeout(() => {
120
+ detailEl.classList.add('hidden');
121
+ detailEl.classList.remove('collapsing');
122
+ }, 200);
123
  }
124
  }
125
 
public/js/tokens.js CHANGED
@@ -1,6 +1,25 @@
1
  // Token管理:增删改查、启用禁用
2
 
3
  let cachedTokens = [];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
 
5
  async function loadTokens() {
6
  try {
@@ -23,19 +42,33 @@ async function loadTokens() {
23
  const refreshingTokens = new Set();
24
 
25
  function renderTokens(tokens) {
26
- cachedTokens = tokens;
 
 
 
27
 
28
  document.getElementById('totalTokens').textContent = tokens.length;
29
  document.getElementById('enabledTokens').textContent = tokens.filter(t => t.enable).length;
30
  document.getElementById('disabledTokens').textContent = tokens.filter(t => !t.enable).length;
31
 
 
 
 
 
 
 
 
 
32
  const tokenList = document.getElementById('tokenList');
33
- if (tokens.length === 0) {
 
 
 
34
  tokenList.innerHTML = `
35
  <div class="empty-state">
36
  <div class="empty-state-icon">📦</div>
37
- <div class="empty-state-text">暂无Token</div>
38
- <div class="empty-state-hint">点击上方OAuth按钮添加Token</div>
39
  </div>
40
  `;
41
  return;
@@ -44,13 +77,17 @@ function renderTokens(tokens) {
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);
@@ -72,20 +109,20 @@ function renderTokens(tokens) {
72
  </span>
73
  <div class="token-header-right">
74
  <button class="btn-icon" onclick="showTokenDetail('${safeRefreshToken}')" title="编辑全部">✏️</button>
75
- <span class="token-id">#${escapeHtml(token.refresh_token.substring(0, 6))}</span>
76
  </div>
77
  </div>
78
  <div class="token-info">
79
- <div class="info-row">
80
  <span class="info-label">🎫</span>
81
  <span class="info-value sensitive-info" title="${safeAccessTokenSuffix}">${safeAccessTokenSuffix}</span>
82
  </div>
83
- <div class="info-row editable" onclick="editField(event, '${safeRefreshToken}', 'projectId', '${safeProjectIdJs}')" title="点击编辑">
84
  <span class="info-label">📦</span>
85
  <span class="info-value sensitive-info">${safeProjectId || '点击设置'}</span>
86
  <span class="info-edit-icon">✏️</span>
87
  </div>
88
- <div class="info-row editable" onclick="editField(event, '${safeRefreshToken}', 'email', '${safeEmailJs}')" title="点击编���">
89
  <span class="info-label">📧</span>
90
  <span class="info-value sensitive-info">${safeEmail || '点击设置'}</span>
91
  <span class="info-edit-icon">✏️</span>
@@ -93,6 +130,7 @@ function renderTokens(tokens) {
93
  <div class="info-row ${isExpired ? 'expired-text' : ''}" id="expire-row-${escapeHtml(cardId)}">
94
  <span class="info-label">⏰</span>
95
  <span class="info-value">${isRefreshing ? '🔄 刷新中...' : escapeHtml(expireStr)}${isExpired && !isRefreshing ? ' (已过期)' : ''}</span>
 
96
  </div>
97
  </div>
98
  <div class="token-quota-inline" id="quota-inline-${escapeHtml(cardId)}">
@@ -104,7 +142,6 @@ function renderTokens(tokens) {
104
  </div>
105
  <div class="token-actions">
106
  <button class="btn btn-info btn-xs" onclick="showQuotaModal('${safeRefreshToken}')" title="查看额度">📊 详情</button>
107
- <button class="btn btn-primary btn-xs" onclick="manualRefreshToken('${safeRefreshToken}')" title="刷新Token" ${isRefreshing ? 'disabled' : ''}>🔄 刷新</button>
108
  <button class="btn ${token.enable ? 'btn-warning' : 'btn-success'} btn-xs" onclick="toggleToken('${safeRefreshToken}', ${!token.enable})" title="${token.enable ? '禁用' : '启用'}">
109
  ${token.enable ? '⏸️ 禁用' : '▶️ 启用'}
110
  </button>
@@ -113,7 +150,7 @@ function renderTokens(tokens) {
113
  </div>
114
  `}).join('');
115
 
116
- tokens.forEach(token => {
117
  loadTokenQuotaSummary(token.refresh_token);
118
  });
119
 
 
1
  // Token管理:增删改查、启用禁用
2
 
3
  let cachedTokens = [];
4
+ let currentFilter = 'all'; // 'all', 'enabled', 'disabled'
5
+
6
+ // 筛选 Token
7
+ function filterTokens(filter) {
8
+ currentFilter = filter;
9
+
10
+ // 更新筛选按钮状态
11
+ document.querySelectorAll('.stat-item').forEach(item => {
12
+ item.classList.remove('active');
13
+ });
14
+ const filterMap = { 'all': 'totalTokens', 'enabled': 'enabledTokens', 'disabled': 'disabledTokens' };
15
+ const activeElement = document.getElementById(filterMap[filter]);
16
+ if (activeElement) {
17
+ activeElement.closest('.stat-item').classList.add('active');
18
+ }
19
+
20
+ // 重新渲染
21
+ renderTokens(cachedTokens);
22
+ }
23
 
24
  async function loadTokens() {
25
  try {
 
42
  const refreshingTokens = new Set();
43
 
44
  function renderTokens(tokens) {
45
+ // 只在首次加载时更新缓存
46
+ if (tokens !== cachedTokens) {
47
+ cachedTokens = tokens;
48
+ }
49
 
50
  document.getElementById('totalTokens').textContent = tokens.length;
51
  document.getElementById('enabledTokens').textContent = tokens.filter(t => t.enable).length;
52
  document.getElementById('disabledTokens').textContent = tokens.filter(t => !t.enable).length;
53
 
54
+ // 根据筛选条件过滤
55
+ let filteredTokens = tokens;
56
+ if (currentFilter === 'enabled') {
57
+ filteredTokens = tokens.filter(t => t.enable);
58
+ } else if (currentFilter === 'disabled') {
59
+ filteredTokens = tokens.filter(t => !t.enable);
60
+ }
61
+
62
  const tokenList = document.getElementById('tokenList');
63
+ if (filteredTokens.length === 0) {
64
+ const emptyText = currentFilter === 'all' ? '暂无Token' :
65
+ currentFilter === 'enabled' ? '暂无启用的Token' : '暂无禁用的Token';
66
+ const emptyHint = currentFilter === 'all' ? '点击上方OAuth按钮添加Token' : '点击上方"总数"查看全部';
67
  tokenList.innerHTML = `
68
  <div class="empty-state">
69
  <div class="empty-state-icon">📦</div>
70
+ <div class="empty-state-text">${emptyText}</div>
71
+ <div class="empty-state-hint">${emptyHint}</div>
72
  </div>
73
  `;
74
  return;
 
77
  // 收集需要自动刷新的过期 Token
78
  const expiredTokensToRefresh = [];
79
 
80
+ tokenList.innerHTML = filteredTokens.map((token, index) => {
81
  const expireTime = new Date(token.timestamp + token.expires_in * 1000);
82
  const isExpired = expireTime < new Date();
83
  const isRefreshing = refreshingTokens.has(token.refresh_token);
84
  const expireStr = expireTime.toLocaleString('zh-CN', {month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit'});
85
  const cardId = token.refresh_token.substring(0, 8);
86
 
87
+ // 计算在原始列表中的序号(基于添加顺序)
88
+ const originalIndex = cachedTokens.findIndex(t => t.refresh_token === token.refresh_token);
89
+ const tokenNumber = originalIndex + 1;
90
+
91
  // 如果已过期且启用状态,加入待刷新列表
92
  if (isExpired && token.enable && !isRefreshing) {
93
  expiredTokensToRefresh.push(token.refresh_token);
 
109
  </span>
110
  <div class="token-header-right">
111
  <button class="btn-icon" onclick="showTokenDetail('${safeRefreshToken}')" title="编辑全部">✏️</button>
112
+ <span class="token-id">#${tokenNumber}</span>
113
  </div>
114
  </div>
115
  <div class="token-info">
116
+ <div class="info-row sensitive-row">
117
  <span class="info-label">🎫</span>
118
  <span class="info-value sensitive-info" title="${safeAccessTokenSuffix}">${safeAccessTokenSuffix}</span>
119
  </div>
120
+ <div class="info-row editable sensitive-row" onclick="editField(event, '${safeRefreshToken}', 'projectId', '${safeProjectIdJs}')" title="点击编辑">
121
  <span class="info-label">📦</span>
122
  <span class="info-value sensitive-info">${safeProjectId || '点击设置'}</span>
123
  <span class="info-edit-icon">✏️</span>
124
  </div>
125
+ <div class="info-row editable sensitive-row" onclick="editField(event, '${safeRefreshToken}', 'email', '${safeEmailJs}')" title="点击编辑">
126
  <span class="info-label">📧</span>
127
  <span class="info-value sensitive-info">${safeEmail || '点击设置'}</span>
128
  <span class="info-edit-icon">✏️</span>
 
130
  <div class="info-row ${isExpired ? 'expired-text' : ''}" id="expire-row-${escapeHtml(cardId)}">
131
  <span class="info-label">⏰</span>
132
  <span class="info-value">${isRefreshing ? '🔄 刷新中...' : escapeHtml(expireStr)}${isExpired && !isRefreshing ? ' (已过期)' : ''}</span>
133
+ <button class="btn-icon btn-refresh" onclick="manualRefreshToken('${safeRefreshToken}')" title="刷新Token" ${isRefreshing ? 'disabled' : ''}>🔄</button>
134
  </div>
135
  </div>
136
  <div class="token-quota-inline" id="quota-inline-${escapeHtml(cardId)}">
 
142
  </div>
143
  <div class="token-actions">
144
  <button class="btn btn-info btn-xs" onclick="showQuotaModal('${safeRefreshToken}')" title="查看额度">📊 详情</button>
 
145
  <button class="btn ${token.enable ? 'btn-warning' : 'btn-success'} btn-xs" onclick="toggleToken('${safeRefreshToken}', ${!token.enable})" title="${token.enable ? '禁用' : '启用'}">
146
  ${token.enable ? '⏸️ 禁用' : '▶️ 启用'}
147
  </button>
 
150
  </div>
151
  `}).join('');
152
 
153
+ filteredTokens.forEach(token => {
154
  loadTokenQuotaSummary(token.refresh_token);
155
  });
156
 
public/js/ui.js CHANGED
@@ -77,15 +77,26 @@ function switchTab(tab, saveState = true) {
77
  targetTab.classList.add('active');
78
  }
79
 
80
- // 隐藏所有页面
81
- document.getElementById('tokensPage').classList.add('hidden');
82
- document.getElementById('settingsPage').classList.add('hidden');
83
 
84
- // 显示对应页面
 
 
 
 
 
 
85
  if (tab === 'tokens') {
86
- document.getElementById('tokensPage').classList.remove('hidden');
 
 
 
87
  } else if (tab === 'settings') {
88
- document.getElementById('settingsPage').classList.remove('hidden');
 
 
 
89
  loadConfig();
90
  }
91
 
 
77
  targetTab.classList.add('active');
78
  }
79
 
80
+ const tokensPage = document.getElementById('tokensPage');
81
+ const settingsPage = document.getElementById('settingsPage');
 
82
 
83
+ // 隐藏所有页面并移除动画类
84
+ tokensPage.classList.add('hidden');
85
+ tokensPage.classList.remove('page-enter');
86
+ settingsPage.classList.add('hidden');
87
+ settingsPage.classList.remove('page-enter');
88
+
89
+ // 显示对应页面并添加入场动画
90
  if (tab === 'tokens') {
91
+ tokensPage.classList.remove('hidden');
92
+ // 触发重排以重新播放动画
93
+ void tokensPage.offsetWidth;
94
+ tokensPage.classList.add('page-enter');
95
  } else if (tab === 'settings') {
96
+ settingsPage.classList.remove('hidden');
97
+ // 触发重排以重新播放动画
98
+ void settingsPage.offsetWidth;
99
+ settingsPage.classList.add('page-enter');
100
  loadConfig();
101
  }
102
 
public/js/utils.js CHANGED
@@ -74,14 +74,12 @@ function updateSensitiveBtn() {
74
  }
75
 
76
  function updateSensitiveInfoDisplay() {
77
- document.querySelectorAll('.sensitive-info').forEach(el => {
 
78
  if (sensitiveInfoHidden) {
79
- el.dataset.original = el.textContent;
80
- el.textContent = '••••••';
81
- el.classList.add('blurred');
82
- } else if (el.dataset.original) {
83
- el.textContent = el.dataset.original;
84
- el.classList.remove('blurred');
85
  }
86
  });
87
  }
 
74
  }
75
 
76
  function updateSensitiveInfoDisplay() {
77
+ // 隐藏/显示包含敏感信息的整行
78
+ document.querySelectorAll('.sensitive-row').forEach(row => {
79
  if (sensitiveInfoHidden) {
80
+ row.style.display = 'none';
81
+ } else {
82
+ row.style.display = '';
 
 
 
83
  }
84
  });
85
  }
public/style.css CHANGED
@@ -268,24 +268,27 @@ button.loading::after {
268
  }
269
 
270
  .tabs { display: flex; gap: 0.25rem; }
271
- .tab {
272
- background: transparent;
273
- color: var(--text-light);
274
- border: none;
275
- padding: 0.5rem 1rem;
276
- border-radius: 0.375rem;
277
- cursor: pointer;
278
- font-weight: 600;
279
- font-size: 0.875rem;
280
- transition: all 0.2s;
281
- min-height: 36px;
282
- box-shadow: none;
 
 
283
  }
284
- .tab:hover { background: var(--bg); color: var(--text); }
 
285
  .tab.active {
286
  background: linear-gradient(135deg, var(--primary), var(--primary-dark));
287
  color: white;
288
- box-shadow: 0 2px 6px rgba(8,145,178,0.25);
289
  }
290
 
291
  .content {
@@ -303,6 +306,9 @@ button.loading::after {
303
  min-height: 0;
304
  overflow: hidden;
305
  }
 
 
 
306
 
307
  #settingsPage {
308
  flex: 1;
@@ -310,6 +316,20 @@ button.loading::after {
310
  overflow-y: auto;
311
  padding-right: 4px;
312
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
313
 
314
  /* 顶部栏 - 统计+操作 */
315
  .top-bar {
@@ -337,6 +357,26 @@ button.loading::after {
337
  align-items: baseline;
338
  gap: 0.25rem;
339
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
340
  .stat-num {
341
  font-size: 1.25rem;
342
  font-weight: 700;
@@ -395,29 +435,52 @@ button.loading::after {
395
  min-height: 0;
396
  }
397
  .token-card {
398
- background: rgba(255, 255, 255, 0.6);
399
  border: 1.5px solid var(--border);
400
  border-radius: 0.75rem;
401
  padding: 0.875rem;
402
- transition: all 0.2s;
403
  position: relative;
 
 
 
 
 
 
 
 
 
 
 
404
  }
 
 
 
 
 
 
 
 
 
 
 
 
405
  @media (prefers-color-scheme: dark) {
406
  .token-card {
407
- background: rgba(30, 41, 59, 0.6);
408
  }
409
  }
410
  .token-card:hover {
411
  border-color: var(--primary);
412
- box-shadow: 0 6px 16px rgba(0,0,0,0.12);
413
- background: rgba(255, 255, 255, 0.85);
414
- transform: scale(1.02);
415
  z-index: 1;
416
  position: relative;
417
  }
418
  @media (prefers-color-scheme: dark) {
419
  .token-card:hover {
420
- background: rgba(30, 41, 59, 0.85);
421
  }
422
  }
423
  .token-card.disabled { opacity: 0.6; }
@@ -546,6 +609,36 @@ button.loading::after {
546
  border-color: var(--primary);
547
  transform: none;
548
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
549
 
550
  .token-actions {
551
  display: flex;
@@ -575,6 +668,17 @@ button.loading::after {
575
  align-items: center;
576
  justify-content: center;
577
  min-height: 300px;
 
 
 
 
 
 
 
 
 
 
 
578
  }
579
  .empty-state-icon { font-size: 3rem; margin-bottom: 0.75rem; opacity: 0.5; }
580
  .empty-state-text { font-size: 1rem; font-weight: 500; margin-bottom: 0.375rem; }
@@ -1183,11 +1287,42 @@ input:checked + .slider:before {
1183
  color: var(--text-light);
1184
  flex-shrink: 0;
1185
  margin-left: 0.5rem;
 
 
 
 
1186
  }
1187
  .quota-inline-detail {
1188
  border-top: 1px solid var(--border);
1189
  padding: 0.5rem;
1190
  background: var(--bg);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1191
  }
1192
  .quota-detail-grid {
1193
  display: flex;
@@ -1264,11 +1399,9 @@ input:checked + .slider:before {
1264
 
1265
  .hidden { display: none !important; }
1266
 
1267
- /* 敏感信息模糊 */
1268
- .sensitive-info.blurred {
1269
- filter: blur(4px);
1270
- user-select: none;
1271
- cursor: default;
1272
  }
1273
 
1274
  /* 响应式 */
 
268
  }
269
 
270
  .tabs { display: flex; gap: 0.25rem; }
271
+ .tab {
272
+ background: transparent;
273
+ color: var(--text-light);
274
+ border: none;
275
+ padding: 0.5rem 1rem;
276
+ border-radius: 0.375rem;
277
+ cursor: pointer;
278
+ font-weight: 600;
279
+ font-size: 0.875rem;
280
+ transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
281
+ min-height: 36px;
282
+ box-shadow: none;
283
+ position: relative;
284
+ overflow: hidden;
285
  }
286
+ .tab:hover { background: var(--bg); color: var(--text); transform: translateY(-1px); }
287
+ .tab:active { transform: translateY(0) scale(0.98); }
288
  .tab.active {
289
  background: linear-gradient(135deg, var(--primary), var(--primary-dark));
290
  color: white;
291
+ box-shadow: 0 2px 8px rgba(8,145,178,0.3);
292
  }
293
 
294
  .content {
 
306
  min-height: 0;
307
  overflow: hidden;
308
  }
309
+ #tokensPage.page-enter {
310
+ animation: pageSlideIn 0.3s cubic-bezier(0.4, 0, 0.2, 1);
311
+ }
312
 
313
  #settingsPage {
314
  flex: 1;
 
316
  overflow-y: auto;
317
  padding-right: 4px;
318
  }
319
+ #settingsPage.page-enter {
320
+ animation: pageSlideIn 0.3s cubic-bezier(0.4, 0, 0.2, 1);
321
+ }
322
+
323
+ @keyframes pageSlideIn {
324
+ from {
325
+ opacity: 0;
326
+ transform: translateX(20px);
327
+ }
328
+ to {
329
+ opacity: 1;
330
+ transform: translateX(0);
331
+ }
332
+ }
333
 
334
  /* 顶部栏 - 统计+操作 */
335
  .top-bar {
 
357
  align-items: baseline;
358
  gap: 0.25rem;
359
  }
360
+ .stat-item.clickable {
361
+ cursor: pointer;
362
+ padding: 0.25rem 0.5rem;
363
+ border-radius: 0.375rem;
364
+ transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
365
+ border: 1.5px solid transparent;
366
+ position: relative;
367
+ }
368
+ .stat-item.clickable:hover {
369
+ background: rgba(8,145,178,0.1);
370
+ transform: translateY(-1px);
371
+ }
372
+ .stat-item.clickable:active {
373
+ transform: translateY(0) scale(0.98);
374
+ }
375
+ .stat-item.clickable.active {
376
+ background: rgba(8,145,178,0.15);
377
+ border-color: var(--primary);
378
+ box-shadow: 0 2px 8px rgba(8,145,178,0.2);
379
+ }
380
  .stat-num {
381
  font-size: 1.25rem;
382
  font-weight: 700;
 
435
  min-height: 0;
436
  }
437
  .token-card {
438
+ background: rgba(255, 255, 255, 0.85);
439
  border: 1.5px solid var(--border);
440
  border-radius: 0.75rem;
441
  padding: 0.875rem;
442
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
443
  position: relative;
444
+ animation: cardFadeIn 0.35s cubic-bezier(0.4, 0, 0.2, 1) backwards;
445
+ }
446
+ @keyframes cardFadeIn {
447
+ from {
448
+ opacity: 0;
449
+ transform: translateY(10px) scale(0.98);
450
+ }
451
+ to {
452
+ opacity: 1;
453
+ transform: translateY(0) scale(1);
454
+ }
455
  }
456
+ /* 为每个卡片添加延迟动画 */
457
+ .token-card:nth-child(1) { animation-delay: 0.02s; }
458
+ .token-card:nth-child(2) { animation-delay: 0.04s; }
459
+ .token-card:nth-child(3) { animation-delay: 0.06s; }
460
+ .token-card:nth-child(4) { animation-delay: 0.08s; }
461
+ .token-card:nth-child(5) { animation-delay: 0.1s; }
462
+ .token-card:nth-child(6) { animation-delay: 0.12s; }
463
+ .token-card:nth-child(7) { animation-delay: 0.14s; }
464
+ .token-card:nth-child(8) { animation-delay: 0.16s; }
465
+ .token-card:nth-child(9) { animation-delay: 0.18s; }
466
+ .token-card:nth-child(10) { animation-delay: 0.2s; }
467
+ .token-card:nth-child(n+11) { animation-delay: 0.22s; }
468
  @media (prefers-color-scheme: dark) {
469
  .token-card {
470
+ background: rgba(30, 41, 59, 0.85);
471
  }
472
  }
473
  .token-card:hover {
474
  border-color: var(--primary);
475
+ box-shadow: 0 8px 24px rgba(0,0,0,0.12);
476
+ background: rgba(255, 255, 255, 0.95);
477
+ transform: translateY(-4px) scale(1.01);
478
  z-index: 1;
479
  position: relative;
480
  }
481
  @media (prefers-color-scheme: dark) {
482
  .token-card:hover {
483
+ background: rgba(30, 41, 59, 0.95);
484
  }
485
  }
486
  .token-card.disabled { opacity: 0.6; }
 
609
  border-color: var(--primary);
610
  transform: none;
611
  }
612
+ .btn-icon.btn-refresh {
613
+ width: 18px;
614
+ height: 18px;
615
+ min-height: 18px;
616
+ min-width: 18px;
617
+ font-size: 0.6rem;
618
+ margin-left: auto;
619
+ flex-shrink: 0;
620
+ background: transparent;
621
+ border: none;
622
+ color: var(--text-light);
623
+ opacity: 0.5;
624
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
625
+ box-shadow: none;
626
+ }
627
+ .btn-icon.btn-refresh:hover:not(:disabled) {
628
+ background: transparent;
629
+ color: var(--primary);
630
+ opacity: 1;
631
+ transform: rotate(180deg);
632
+ }
633
+ .btn-icon.btn-refresh:active:not(:disabled) {
634
+ transform: rotate(360deg);
635
+ }
636
+ .btn-icon.btn-refresh:disabled {
637
+ opacity: 0.8;
638
+ cursor: not-allowed;
639
+ color: var(--warning);
640
+ animation: spin 0.8s linear infinite;
641
+ }
642
 
643
  .token-actions {
644
  display: flex;
 
668
  align-items: center;
669
  justify-content: center;
670
  min-height: 300px;
671
+ animation: emptyFadeIn 0.4s cubic-bezier(0.4, 0, 0.2, 1);
672
+ }
673
+ @keyframes emptyFadeIn {
674
+ from {
675
+ opacity: 0;
676
+ transform: scale(0.95);
677
+ }
678
+ to {
679
+ opacity: 1;
680
+ transform: scale(1);
681
+ }
682
  }
683
  .empty-state-icon { font-size: 3rem; margin-bottom: 0.75rem; opacity: 0.5; }
684
  .empty-state-text { font-size: 1rem; font-weight: 500; margin-bottom: 0.375rem; }
 
1287
  color: var(--text-light);
1288
  flex-shrink: 0;
1289
  margin-left: 0.5rem;
1290
+ transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
1291
+ }
1292
+ .quota-inline-toggle.expanded {
1293
+ transform: rotate(180deg);
1294
  }
1295
  .quota-inline-detail {
1296
  border-top: 1px solid var(--border);
1297
  padding: 0.5rem;
1298
  background: var(--bg);
1299
+ animation: slideDown 0.25s cubic-bezier(0.4, 0, 0.2, 1);
1300
+ transform-origin: top;
1301
+ }
1302
+ @keyframes slideDown {
1303
+ from {
1304
+ opacity: 0;
1305
+ transform: scaleY(0.8);
1306
+ max-height: 0;
1307
+ }
1308
+ to {
1309
+ opacity: 1;
1310
+ transform: scaleY(1);
1311
+ max-height: 500px;
1312
+ }
1313
+ }
1314
+ .quota-inline-detail.collapsing {
1315
+ animation: slideUp 0.2s cubic-bezier(0.4, 0, 0.2, 1) forwards;
1316
+ }
1317
+ @keyframes slideUp {
1318
+ from {
1319
+ opacity: 1;
1320
+ transform: scaleY(1);
1321
+ }
1322
+ to {
1323
+ opacity: 0;
1324
+ transform: scaleY(0.8);
1325
+ }
1326
  }
1327
  .quota-detail-grid {
1328
  display: flex;
 
1399
 
1400
  .hidden { display: none !important; }
1401
 
1402
+ /* 敏感信息行 - 隐藏时不显示 */
1403
+ .sensitive-row {
1404
+ transition: opacity 0.2s, max-height 0.2s;
 
 
1405
  }
1406
 
1407
  /* 响应式 */