augment2api / templates /admin.html
github-actions[bot]
Update from GitHub Actions
db3a988
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Augment2Api-Panel</title>
<link rel="icon" href="../static/augment.svg" type="image/svg+xml">
<link rel="alternate icon" href="../static/augment.svg" type="image/x-icon">
<style>
:root {
--primary-color: #4a6cf7;
--primary-hover: #3a5ce4;
--bg-color: #f5f7fa;
--card-bg: #ffffff;
--text-color: #333333;
--text-secondary: #6c757d;
--border-color: #e9ecef;
--header-bg: #ffffff;
--header-color: #333333;
--sidebar-bg: #ffffff;
--sidebar-color: #333333;
--sidebar-hover: #f0f4ff;
--sidebar-active: #e6edff;
--footer-bg: #ffffff;
--footer-color: #6c757d;
--success-color: #28a745;
--error-color: #dc3545;
--warning-color: #ffc107;
--radius: 8px;
--shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
--transition: all 0.3s ease;
}
html, body {
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}
body {
display: flex;
flex-direction: column;
background-color: var(--bg-color);
color: var(--text-color);
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
}
.container {
display: flex;
flex-direction: column;
height: 100vh;
width: 100%;
max-width: 100%;
margin: 0;
padding: 0;
}
header {
background-color: var(--header-bg);
color: var(--header-color);
padding: 8px 20px;
box-shadow: var(--shadow);
z-index: 10;
border-bottom: 1px solid var(--border-color);
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
height: 40px; /* 固定高度 */
}
.header-content h1 {
font-size: 18px;
margin: 0;
font-weight: 600;
}
.logout-btn {
background-color: var(--primary-color);
color: white;
border: none;
border-radius: var(--radius);
padding: 6px 12px;
cursor: pointer;
font-size: 14px;
transition: var(--transition);
display: flex;
align-items: center;
gap: 5px;
}
.logout-btn:hover {
background-color: var(--primary-hover);
}
.dashboard {
display: flex;
flex: 1;
height: calc(100vh - 100px);
overflow: hidden;
margin: 0;
padding: 15px;
gap: 15px;
}
.sidebar {
width: 200px;
height: 100%;
background-color: var(--sidebar-bg);
color: var(--sidebar-color);
transition: width 0.3s ease;
border-radius: var(--radius);
box-shadow: var(--shadow);
overflow: hidden;
display: flex;
flex-direction: column;
}
.sidebar.collapsed {
width: 80px;
}
.sidebar.collapsed .sidebar-header h3 {
display: none;
}
.sidebar-header {
padding: 15px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid var(--border-color);
}
.sidebar-header h3 {
margin: 0;
color: var(--text-color);
font-size: 16px;
font-weight: 600;
white-space: nowrap;
}
.toggle-btn {
background: transparent;
border: none;
color: var(--text-secondary);
cursor: pointer;
font-size: 16px;
padding: 5px;
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.3s, background-color 0.2s;
border-radius: 4px;
}
.toggle-btn:hover {
background-color: #f1f1f1;
}
.sidebar.collapsed .toggle-btn {
transform: rotate(180deg);
}
.sidebar-menu {
display: flex;
flex-direction: column;
padding: 10px 0;
flex: 1;
}
.menu-item {
padding: 10px 15px;
display: flex;
align-items: center;
cursor: pointer;
transition: var(--transition);
white-space: nowrap;
border-radius: 4px;
margin: 2px 8px;
}
.menu-item:hover {
background-color: var(--sidebar-hover);
color: var(--primary-color);
}
.menu-item.active {
background-color: var(--sidebar-active);
color: var(--primary-color);
font-weight: 500;
}
.menu-item i {
font-size: 18px;
margin-right: 12px;
}
.sidebar.collapsed .menu-item {
padding: 10px 8px;
justify-content: center;
}
.sidebar.collapsed .menu-text {
display: none;
}
.sidebar.collapsed .menu-item i {
margin-right: 0;
font-size: 20px;
}
.sidebar.collapsed .sidebar-header {
justify-content: center;
padding: 15px 5px;
}
/* 主内容区样式优化 */
.main-content {
flex: 1;
height: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
background-color: var(--card-bg);
border-radius: var(--radius);
box-shadow: var(--shadow);
}
.content-panel {
padding: 20px;
display: none;
flex: 1;
overflow-y: auto;
height: 100%;
flex-direction: column;
}
.content-panel.active {
display: flex;
flex-direction: column;
}
/* 添加token列表容器样式,使其可滚动 */
.token-list-container {
max-height: calc(100vh - 250px);
overflow-y: auto;
overflow-x: hidden;
padding-right: 10px;
position: relative;
}
/* 添加列表加载效果 */
.token-list-loading {
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
color: var(--text-secondary);
}
.token-list-loading .spinner {
width: 20px;
height: 20px;
border: 2px solid var(--border-color);
border-top-color: var(--primary-color);
border-radius: 50%;
animation: spin 1s linear infinite;
margin-right: 10px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* 面板标题样式优化 */
.panel-title {
display: flex;
align-items: center;
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 1px solid var(--border-color);
}
.panel-title h2 {
margin: 0 0 0 10px;
font-size: 18px;
font-weight: 600;
color: var(--text-color);
}
.panel-title i {
font-size: 20px;
color: var(--primary-color);
}
.panel-actions {
margin-left: auto;
display: flex;
gap: 10px;
}
/* 按钮样式统一 */
button {
background-color: var(--primary-color);
color: white;
border: none;
border-radius: var(--radius);
padding: 8px 12px;
cursor: pointer;
font-size: 14px;
transition: var(--transition);
display: flex;
align-items: center;
gap: 5px;
}
button:hover {
background-color: var(--primary-hover);
}
button.secondary {
background-color: transparent;
color: var(--text-color);
border: 1px solid var(--border-color);
}
button.secondary:hover {
background-color: #f8f9fa;
}
button.secondary i {
color: var(--text-color);
}
/* Token列表样式优化 */
.token-list {
display: flex;
flex-direction: column;
gap: 10px;
margin-bottom: 15px;
}
.token-item {
border: 1px solid var(--border-color);
border-radius: var(--radius);
overflow: hidden;
transition: var(--transition);
margin-bottom: 10px;
}
.token-item:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.token-header {
display: flex;
align-items: center;
padding: 12px 15px;
background-color: #f8f9fa;
cursor: pointer;
}
.token-number {
width: 30px;
font-weight: 500;
color: var(--text-secondary);
}
.token-summary {
flex: 1;
font-family: monospace;
color: var(--text-color);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 13px;
display: flex;
align-items: center;
gap: 8px;
}
.token-remark {
background-color: #e3f2fd;
color: #1976d2;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
font-family: system-ui;
cursor: pointer;
border: 1px solid transparent;
transition: all 0.2s;
}
.token-remark:hover {
border-color: #1976d2;
}
.token-remark.empty {
background-color: #f5f5f5;
color: #9e9e9e;
}
.token-remark input {
background: none;
border: none;
outline: none;
font-size: inherit;
font-family: inherit;
color: inherit;
width: 100%;
min-width: 100px;
}
.token-usage-count {
display: flex;
align-items: center;
justify-content: center;
color: var(--text-color);
font-size: 13px;
font-weight: 500;
margin-left: 10px;
}
/* 根据使用次数变化颜色 - 只应用于数字 */
.token-usage-count .low {
color: #28a745; /* 绿色 - 使用次数少 */
}
.token-usage-count .medium {
color: #ffc107; /* 黄色 - 使用次数中等 */
}
.token-usage-count .high {
color: #dc3545; /* 红色 - 使用次数多 */
}
.token-toggle {
margin-left: 10px;
transition: transform 0.3s;
}
.token-toggle.open i {
transform: rotate(180deg);
}
.token-details {
padding: 0;
max-height: 0;
overflow: hidden;
transition: all 0.3s ease;
background-color: #ffffff;
}
.token-details.open {
padding: 15px;
max-height: 200px;
border-top: 1px solid var(--border-color);
overflow-y: auto;
}
.token-label {
font-weight: 500;
margin-bottom: 5px;
color: var(--text-secondary);
font-size: 13px;
}
.token-display {
padding: 8px 10px;
background-color: #f8f9fa;
border-radius: 4px;
font-family: monospace;
margin-bottom: 10px;
word-break: break-all;
font-size: 13px;
overflow-x: auto;
}
.token-actions {
display: flex;
justify-content: flex-end;
margin-top: 10px;
}
.delete-token {
background-color: var(--error-color);
}
.delete-token:hover {
background-color: #c82333;
}
/* 分页控件样式优化 */
.pagination-container {
display: flex;
justify-content: center;
align-items: center;
padding: 15px 0;
border-top: 1px solid var(--border-color);
}
.pagination-btn {
background-color: transparent;
border: 1px solid var(--border-color);
color: var(--text-color);
border-radius: var(--radius);
padding: 6px 10px;
margin: 0 5px;
cursor: pointer;
transition: var(--transition);
}
.pagination-btn:hover:not([disabled]) {
background-color: var(--primary-color);
color: white;
border-color: var(--primary-color);
}
.pagination-btn[disabled] {
opacity: 0.5;
cursor: not-allowed;
}
#page-info {
margin: 0 15px;
font-size: 14px;
color: var(--text-secondary);
}
.page-size-select {
margin-left: 15px;
padding: 6px 8px;
border-radius: var(--radius);
border: 1px solid var(--border-color);
background-color: white;
color: var(--text-color);
font-size: 14px;
cursor: pointer;
}
/* 页脚样式优化 */
footer {
text-align: center;
padding: 10px 0;
background-color: var(--footer-bg);
color: var(--footer-color);
font-size: 13px;
border-top: 1px solid var(--border-color);
}
footer a {
color: var(--primary-color);
text-decoration: none;
}
footer a:hover {
text-decoration: underline;
}
/* 响应式设计优化 */
@media (max-width: 768px) {
.dashboard {
flex-direction: column;
height: auto;
padding: 10px;
}
.sidebar {
width: 100% !important;
margin-bottom: 15px;
max-height: 200px;
}
.main-content {
height: calc(100vh - 300px);
overflow-y: auto;
}
.content-panel {
padding: 15px;
max-height: none;
}
.sidebar.collapsed .menu-text {
display: inline;
}
.toggle-btn {
display: none;
}
}
@media (max-width: 1200px) {
.token-display {
max-width: 100%;
white-space: nowrap;
}
}
@media (min-width: 1201px) {
.token-display {
white-space: normal;
}
}
.token-details.open .token-display {
white-space: normal;
word-break: break-all;
}
/* 添加Token面板样式优化 */
.auth-steps {
display: flex;
flex-direction: column;
gap: 25px;
padding-bottom: 20px;
}
.step {
background-color: #f8f9fa;
border-radius: var(--radius);
padding: 20px;
border: 1px solid var(--border-color);
}
.step h3 {
display: flex;
align-items: center;
margin-top: 0;
margin-bottom: 15px;
font-size: 16px;
font-weight: 600;
}
.step-number {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
background-color: var(--primary-color);
color: white;
border-radius: 50%;
margin-right: 10px;
font-size: 14px;
}
.step p {
margin-bottom: 15px;
color: var(--text-secondary);
}
textarea {
width: 100%;
padding: 10px;
border: 1px solid var(--border-color);
border-radius: var(--radius);
font-family: monospace;
min-height: 100px;
margin-bottom: 10px;
resize: vertical;
}
.error {
color: var(--error-color);
margin: 10px 0;
display: none;
}
.success {
color: var(--success-color);
margin: 10px 0;
display: none;
}
button:not(.secondary):not(.pagination-btn):not(.toggle-btn) i {
color: white;
}
/* 刷新按钮加载动画 */
.refresh-btn, #check-all-tokens {
position: relative;
}
.refresh-btn i, #check-all-tokens i {
transition: transform 0.3s ease;
}
.refresh-btn.loading i, #check-all-tokens.loading i {
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 禁用状态 */
.refresh-btn.loading, #check-all-tokens.loading {
pointer-events: none;
opacity: 0.7;
}
/* 检测结果样式 */
.check-result {
color: var(--success-color);
background-color: rgba(40, 167, 69, 0.1);
border: 1px solid var(--success-color);
border-radius: var(--radius);
padding: 10px 15px;
margin: 10px 0;
display: none;
}
/* 弹出输入框样式 */
.remark-input-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.remark-input-container {
background-color: white;
padding: 20px;
border-radius: var(--radius);
box-shadow: var(--shadow);
width: 250px;
}
.remark-input-container h3 {
margin: 0 0 15px 0;
font-size: 16px;
color: var(--text-color);
}
.remark-input-container input {
width: 100%;
padding: 8px 10px;
border: 1px solid var(--border-color);
border-radius: 4px;
margin-bottom: 15px;
font-size: 14px;
box-sizing: border-box;
}
.remark-input-container .char-count {
font-size: 12px;
color: var(--text-secondary);
margin-bottom: 15px;
text-align: right;
}
.remark-input-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
}
/* Token模糊化样式 */
.token-blur {
filter: blur(3px);
transition: all 0.3s ease;
}
.token-blur:hover {
filter: blur(0);
}
/* 删除背景颜色变化样式 */
#toggle-token-visibility.active i {
color: var(--primary-color);
}
/* 冷却状态图标样式 */
.cool-status {
color: #1e88e5;
font-size: 18px;
margin-left: 5px;
vertical-align: middle;
}
.cool-status-tooltip {
position: relative;
display: inline-block;
}
.cool-status-tooltip .tooltip-text {
visibility: hidden;
width: 200px;
background-color: #333;
color: #fff;
text-align: center;
border-radius: 6px;
padding: 5px;
position: absolute;
z-index: 1;
bottom: 125%;
left: 50%;
margin-left: -100px;
opacity: 0;
transition: opacity 0.3s;
font-size: 12px;
}
.cool-status-tooltip:hover .tooltip-text {
visibility: visible;
opacity: 1;
}
</style>
<!-- 添加图标 -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
</head>
<body>
<div class="container">
<header>
<div class="header-content">
<h1>Augment面板|v1.0.6</h1>
<button id="logout-btn" class="logout-btn">
<i class="bi bi-box-arrow-right"></i> 登出
</button>
</div>
</header>
<div class="dashboard">
<!-- 左侧导航栏 -->
<div class="sidebar" id="sidebar">
<div class="sidebar-header">
<h3>面板功能导航</h3>
<button id="toggle-sidebar" class="toggle-btn">
<i class="bi bi-chevron-left"></i>
</button>
</div>
<div class="sidebar-menu">
<div class="menu-item active" data-target="token-list-panel">
<i class="bi bi-list-ul"></i>
<span class="menu-text">Token列表</span>
</div>
<div class="menu-item" data-target="token-add-panel">
<i class="bi bi-plus-circle"></i>
<span class="menu-text">添加Token</span>
</div>
</div>
</div>
<!-- 右侧主内容区 -->
<div class="main-content" id="main-content">
<!-- Token列表面板 -->
<div class="content-panel active" id="token-list-panel">
<div class="panel-title">
<i class="bi bi-key-fill"></i>
<h2>Token列表</h2>
<div class="panel-actions">
<button id="toggle-token-visibility" class="secondary">
<i class="bi bi-eye-slash"></i> 隐藏Token
</button>
<button id="refresh-token" class="refresh-btn">
<i class="bi bi-arrow-clockwise"></i> 刷新列表
</button>
<button id="check-all-tokens"><i class="bi bi-shield-check btn-icon"></i> <span class="btn-text">批量检测</span></button>
</div>
</div>
<!-- 添加可滚动容器 -->
<div class="token-list-container">
<div id="token-list">加载中...</div>
</div>
<!-- 分页控件 -->
<div class="pagination-container" id="pagination-container">
<button class="pagination-btn" id="prev-page" disabled><i class="bi bi-chevron-left"></i></button>
<span id="page-info"><span id="current-page">1</span> 页,共 <span id="total-pages">1</span></span>
<button class="pagination-btn" id="next-page"><i class="bi bi-chevron-right"></i></button>
<select id="page-size" class="page-size-select">
<option value="10">10条/页</option>
<option value="20">20条/页</option>
<option value="50">50条/页</option>
</select>
</div>
</div>
<!-- 添加Token面板 -->
<div class="content-panel" id="token-add-panel">
<div class="panel-title">
<i class="bi bi-shield-lock-fill"></i>
<h2>授权获取Token</h2>
</div>
<div class="auth-steps">
<div class="step">
<h3><span class="step-number">1</span> 获取授权地址</h3>
<p>点击下方按钮获取授权地址,然后在浏览器中打开该地址进行授权。</p>
<button id="get-auth-url"><i class="bi bi-link-45deg" class="btn-icon"></i> <span class="btn-text">获取授权地址</span></button>
<div id="auth-url" class="token-display" style="display: none;"></div>
</div>
<div class="step">
<h3><span class="step-number">2</span> 提交授权响应</h3>
<p>完成授权后,将获得的授权响应粘贴到下面的文本框中:</p>
<textarea id="auth-response" placeholder='{"code":"_000baec407c57c4bf9xxxxxxxxxxxxxx","state":"0uXxxxxxxxx","tenant_url":"https://dxx.api.augmentcode.com/"}'></textarea>
<div id="validation-message" class="error"></div>
<button id="submit-auth"><i class="bi bi-check2-circle" class="btn-text"></i> <span class="btn-text">获取Token</span></button>
<div id="submit-result" class="success"></div>
</div>
</div>
</div>
</div>
</div>
<!-- 添加页脚 -->
<footer>
<a href="https://linux.do/u/bifang/summary" target="_blank">开发者:彼方</a> | <a href="https://2api-docs.pages.dev/page/augment2api/func-intro" target="_blank">文档中心</a>
</footer>
</div>
<script>
// 检查会话是否有效
function checkSession() {
// 从Cookie中获取token
const cookies = document.cookie.split(';');
let token = null;
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.startsWith('auth_token=')) {
token = cookie.substring('auth_token='.length);
break;
}
}
if (!token) {
window.location.href = '/login';
return false;
}
// 将token添加到所有API请求中
return token;
}
// 页面加载时检查会话
const authToken = checkSession();
if (!authToken) {
// 如果没有有效会话,不继续执行后续代码
throw new Error('No valid session');
}
// 为所有fetch请求添加认证头
const originalFetch = window.fetch;
window.fetch = function(url, options = {}) {
// 创建新的options对象,避免修改原始对象
const newOptions = { ...options };
// 确保headers对象存在
newOptions.headers = newOptions.headers || {};
// 如果是对象形式,转换为Headers对象
if (!(newOptions.headers instanceof Headers)) {
const headers = new Headers(newOptions.headers);
headers.append('X-Auth-Token', authToken);
newOptions.headers = headers;
} else {
newOptions.headers.append('X-Auth-Token', authToken);
}
return originalFetch(url, newOptions);
};
document.addEventListener('DOMContentLoaded', function() {
// 侧边栏切换
const sidebar = document.getElementById('sidebar');
const toggleBtn = document.getElementById('toggle-sidebar');
const menuItems = document.querySelectorAll('.menu-item');
const contentPanels = document.querySelectorAll('.content-panel');
// 分页变量
let currentPage = 1;
let pageSize = 10;
let allTokens = [];
// 侧边栏折叠/展开
toggleBtn.addEventListener('click', function() {
sidebar.classList.toggle('collapsed');
});
// 菜单项切换
menuItems.forEach(item => {
item.addEventListener('click', function() {
// 移除所有菜单项的active类
menuItems.forEach(i => i.classList.remove('active'));
// 为当前点击的菜单项添加active类
this.classList.add('active');
// 获取目标面板ID
const targetId = this.getAttribute('data-target');
// 隐藏所有内容面板
contentPanels.forEach(panel => {
panel.classList.remove('active');
});
// 显示目标面板
document.getElementById(targetId).classList.add('active');
});
});
// 分页功能
const prevPageBtn = document.getElementById('prev-page');
const nextPageBtn = document.getElementById('next-page');
const currentPageSpan = document.getElementById('current-page');
const totalPagesSpan = document.getElementById('total-pages');
const pageSizeSelect = document.getElementById('page-size');
// 页面大小变化
pageSizeSelect.addEventListener('change', function() {
pageSize = parseInt(this.value);
currentPage = 1;
fetchCurrentToken();
});
// 上一页
prevPageBtn.addEventListener('click', function() {
if (currentPage > 1) {
currentPage--;
fetchCurrentToken();
}
});
// 下一页
nextPageBtn.addEventListener('click', function() {
currentPage++;
fetchCurrentToken();
});
// 添加节流功能,避免短时间内多次请求
function throttle(func, delay) {
let lastCall = 0;
return function(...args) {
const now = new Date().getTime();
if (now - lastCall < delay) {
return;
}
lastCall = now;
return func.apply(this, args);
};
}
// 添加简单的缓存机制
const tokenCache = {
data: {},
timestamp: 0,
ttl: 10000, // 缓存有效期10秒
get: function(key) {
const now = new Date().getTime();
if (this.data[key] && (now - this.timestamp < this.ttl)) {
return this.data[key];
}
return null;
},
set: function(key, data) {
this.data[key] = data;
this.timestamp = new Date().getTime();
}
};
// 修改获取当前Token列表函数,使用后端分页
const fetchCurrentToken = throttle(function() {
// 显示加载动画
const refreshBtn = document.getElementById('refresh-token');
const tokenListElement = document.getElementById('token-list');
refreshBtn.classList.add('loading');
// 构造缓存键
const cacheKey = `tokens_${currentPage}_${pageSize}`;
// 检查缓存
const cachedData = tokenCache.get(cacheKey);
if (cachedData && !forceFresh) {
// 使用缓存数据
allTokens = cachedData.tokens || [];
renderTokenList(cachedData.total || 0, cachedData.total_pages || 1);
refreshBtn.classList.remove('loading');
return;
}
// 重置强制刷新标志
forceFresh = false;
// 设置超时处理
const timeoutId = setTimeout(() => {
refreshBtn.classList.remove('loading');
tokenListElement.innerHTML = '<div class="error" style="display:block;">请求超时,请重试</div>';
}, 10000); // 10秒超时
// 添加性能标记
const startTime = performance.now();
fetch(`/api/tokens?page=${currentPage}&page_size=${pageSize}`)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
clearTimeout(timeoutId); // 清除超时定时器
// 记录加载时间
const loadTime = performance.now() - startTime;
console.log(`Token列表加载耗时: ${loadTime.toFixed(2)}ms`);
if (data.status === 'success') {
// 缓存结果
tokenCache.set(cacheKey, data);
// 使用后端返回的token列表
allTokens = data.tokens || [];
// 更新分页信息
const totalItems = data.total || 0;
const totalPages = data.total_pages || 1;
currentPage = data.page || 1;
// 渲染token列表和分页控件
renderTokenList(totalItems, totalPages);
} else {
tokenListElement.innerHTML =
'<div class="error" style="display:block;">获取Token列表失败: ' + (data.error || '未知错误') + '</div>';
}
})
.catch(error => {
clearTimeout(timeoutId); // 清除超时定时器
tokenListElement.innerHTML =
'<div class="error" style="display:block;">请求失败: ' + error.message + '</div>';
})
.finally(() => {
refreshBtn.classList.remove('loading');
});
}, 300); // 300ms节流
// 添加强制刷新标记
let forceFresh = false;
// 刷新Token按钮事件
document.getElementById('refresh-token').addEventListener('click', function() {
forceFresh = true; // 强制刷新,忽略缓存
fetchCurrentToken();
});
// 修改渲染token列表函数,使用后端返回的分页信息
function renderTokenList(totalItems, totalPages) {
const tokenListElement = document.getElementById('token-list');
// 如果没有token
if (totalItems === 0) {
tokenListElement.innerHTML = '<div class="no-tokens">暂无可用Token,请点击"添加Token"获取</div>';
// 隐藏分页控件
document.getElementById('pagination-container').style.display = 'none';
return;
}
// 显示分页控件
document.getElementById('pagination-container').style.display = 'flex';
// 更新分页信息显示
totalPagesSpan.textContent = totalPages;
currentPageSpan.textContent = currentPage;
// 更新分页按钮状态
prevPageBtn.disabled = currentPage === 1;
nextPageBtn.disabled = currentPage === totalPages;
// 检查token数量,如果过多则使用分批渲染
const useBatchRendering = allTokens.length > 50;
// 优化:使用文档片段减少DOM重绘次数
const fragment = document.createDocumentFragment();
const tokenList = document.createElement('div');
tokenList.className = 'token-list';
fragment.appendChild(tokenList);
// 清空现有内容并显示加载中
tokenListElement.innerHTML = '';
if (useBatchRendering) {
// 先显示加载状态
const loadingEl = document.createElement('div');
loadingEl.className = 'token-list-loading';
loadingEl.innerHTML = '<div class="spinner"></div> <span>正在加载Token列表...</span>';
tokenListElement.appendChild(loadingEl);
// 使用requestAnimationFrame和分批处理来渲染大列表
setTimeout(() => {
renderTokensBatch(tokenList, 0, 20);
}, 10);
function renderTokensBatch(container, startIdx, batchSize) {
// 移除加载状态
const loadingEl = tokenListElement.querySelector('.token-list-loading');
if (loadingEl) {
loadingEl.remove();
}
// 添加当前批次的token
const endIdx = Math.min(startIdx + batchSize, allTokens.length);
for (let i = startIdx; i < endIdx; i++) {
const tokenItem = createTokenItem(allTokens[i], i);
container.appendChild(tokenItem);
}
// 如果还有更多token待渲染,安排下一批
if (endIdx < allTokens.length) {
// 添加临时"加载更多"指示器
if (startIdx === 0) { // 只在第一批后添加DOM
tokenListElement.appendChild(fragment);
}
setTimeout(() => {
requestAnimationFrame(() => {
renderTokensBatch(container, endIdx, batchSize);
});
}, 0);
} else {
// 全部渲染完成,如果是第一批,添加到DOM
if (startIdx === 0) {
tokenListElement.appendChild(fragment);
}
// 应用Token可见性
if (!tokenVisible) {
applyTokenVisibility();
}
}
}
} else {
// 直接渲染所有token
allTokens.forEach((tokenInfo, index) => {
const tokenItem = createTokenItem(tokenInfo, index);
tokenList.appendChild(tokenItem);
});
// 一次性替换DOM内容
tokenListElement.appendChild(fragment);
// 应用Token可见性
if (!tokenVisible) {
applyTokenVisibility();
}
}
// 辅助函数:创建token项元素
function createTokenItem(tokenInfo, index) {
// 计算在当前页中的索引
const displayIndex = index + 1 + (currentPage - 1) * pageSize;
// 获取使用次数并设置样式类
const chatUsageCount = tokenInfo.chat_usage_count || 0;
const agentUsageCount = tokenInfo.agent_usage_count || 0;
let usageClass = '';
// 根据CHAT和AGENT模式的使用次数来确定样式类
if (chatUsageCount < 1000 && agentUsageCount < 20) {
usageClass = 'low';
} else if (chatUsageCount < 2000 && agentUsageCount < 40) {
usageClass = 'medium';
} else {
usageClass = 'high';
}
const tokenItem = document.createElement('div');
tokenItem.className = 'token-item';
tokenItem.dataset.index = index;
tokenItem.innerHTML = `
<div class="token-header" data-index="${index}">
<div class="token-number">${displayIndex}</div>
<div class="token-summary">
${tokenInfo.token}
<span class="token-remark${!tokenInfo.remark ? ' empty' : ''}" data-token="${tokenInfo.token}" data-remark="${tokenInfo.remark || ''}">${tokenInfo.remark || '添加备注'}</span>
${tokenInfo.in_cool ? `
<span class="cool-status-tooltip">
<i class="bi bi-snow cool-status"></i>
<span class="tooltip-text">冷却中,直到: ${new Date(tokenInfo.cool_end).toLocaleString()}</span>
</span>` : ''}
</div>
<div class="token-usage-count">
CHAT使用:&nbsp;&nbsp; <span class="${usageClass}">${chatUsageCount}</span>&nbsp;&nbsp;次 | AGENT使用:&nbsp;&nbsp; <span class="${usageClass}">${agentUsageCount}</span>&nbsp;&nbsp;次
</div>
<div class="token-toggle"><i class="bi bi-chevron-down"></i></div>
</div>
<div class="token-details">
<div class="token-label">Token:</div>
<div class="token-display">${tokenInfo.token}</div>
<div class="token-label">租户URL:</div>
<div class="token-display">${tokenInfo.tenant_url}</div>
<div class="token-actions">
<button class="delete-token" data-token="${tokenInfo.token}">
<i class="bi bi-trash"></i> 删除
</button>
</div>
</div>
`;
return tokenItem;
}
// 使用事件委托处理所有点击事件
if (!document.querySelector('.token-list').hasEventListener) {
document.querySelector('.token-list').addEventListener('click', function(e) {
// 处理备注点击
const remarkElement = e.target.closest('.token-remark');
if (remarkElement) {
e.stopPropagation(); // 阻止事件冒泡到header
const token = remarkElement.dataset.token;
const currentRemark = remarkElement.dataset.remark;
// 创建弹出层
const modal = document.createElement('div');
modal.className = 'remark-input-modal';
modal.innerHTML = `
<div class="remark-input-container">
<h3>编辑备注</h3>
<input type="text" maxlength="30" placeholder="请输入备注(30字以内)" value="${currentRemark}">
<div class="char-count"><span>${currentRemark.length}</span>/30</div>
<div class="remark-input-actions">
<button class="secondary" onclick="this.closest('.remark-input-modal').remove()">取消</button>
<button class="save-remark" data-token="${token}">保存</button>
</div>
</div>
`;
document.body.appendChild(modal);
// 获取输入框并聚焦
const input = modal.querySelector('input');
input.focus();
// 更新字符计数
input.addEventListener('input', () => {
const count = input.value.length;
modal.querySelector('.char-count span').textContent = count;
});
// 点击背景关闭弹窗
modal.addEventListener('click', (e) => {
if (e.target === modal) {
modal.remove();
}
});
// 处理保存按钮点击
modal.querySelector('.save-remark').addEventListener('click', async function() {
const token = this.dataset.token;
const newRemark = input.value.trim();
try {
const response = await fetch(`/api/token/${token}/remark`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ remark: newRemark })
});
const data = await response.json();
if (data.status === 'success') {
// 更新所有具有相同token的备注元素
document.querySelectorAll(`.token-remark[data-token="${token}"]`).forEach(el => {
el.textContent = newRemark || '添加备注';
el.dataset.remark = newRemark;
if (newRemark) {
el.classList.remove('empty');
} else {
el.classList.add('empty');
}
});
// 关闭弹窗
modal.remove();
} else {
alert('更新备注失败: ' + (data.error || '未知错误'));
}
} catch (error) {
alert('请求失败: ' + error.message);
}
});
return; // 阻止后续处理
}
// 处理折叠/展开
const headerElement = e.target.closest('.token-header');
if (headerElement && !e.target.closest('.token-remark')) {
const tokenItem = headerElement.closest('.token-item');
const details = tokenItem.querySelector('.token-details');
const toggle = tokenItem.querySelector('.token-toggle');
details.classList.toggle('open');
toggle.classList.toggle('open');
}
});
// 标记已添加过事件监听器
document.querySelector('.token-list').hasEventListener = true;
}
}
// 为token列表添加事件委托,只处理删除按钮
document.getElementById('token-list').addEventListener('click', function(e) {
// 检查点击的是否是删除按钮
if (e.target.closest('.delete-token')) {
const deleteBtn = e.target.closest('.delete-token');
const token = deleteBtn.getAttribute('data-token');
if (confirm('确定要删除此Token吗?')) {
// 发送删除请求
fetch(`/api/token/${encodeURIComponent(token)}`, {
method: 'DELETE'
})
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
// 刷新token列表
fetchCurrentToken();
} else {
alert('删除失败: ' + (data.error || '未知错误'));
}
})
.catch(error => {
alert('请求失败: ' + error.message);
});
}
}
});
// 添加登出处理逻辑
document.getElementById('logout-btn').addEventListener('click', function() {
if(confirm('确定要登出吗?')) {
fetch('/api/logout', {
method: 'POST',
headers: {
'X-Auth-Token': authToken
}
})
.then(response => response.json())
.then(data => {
if(data.status === 'success') {
// 清除Cookie
document.cookie = "auth_token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT";
// 重定向到登录页
window.location.href = '/login';
} else {
alert('登出失败: ' + (data.error || '未知错误'));
}
})
.catch(error => {
alert('登出请求失败: ' + error.message);
});
}
});
// 批量检测token
document.getElementById('check-all-tokens').addEventListener('click', function() {
const button = this;
button.classList.add('loading');
fetch('/api/check-tokens')
.then(response => response.json())
.then(data => {
if(data.status === 'success') {
// 创建或获取检测结果显示元素
let checkResult = document.querySelector('.check-result');
if(!checkResult) {
checkResult = document.createElement('div');
checkResult.className = 'check-result';
document.querySelector('.panel-title').after(checkResult);
}
// 显示检测结果
checkResult.textContent = `检测完成! 共检测 ${data.total} 个Token,更新 ${data.updated} 个Token租户地址,禁用 ${data.disabled} 个无效Token`;
checkResult.style.display = 'block';
// 如果有更新或禁用,则刷新token列表
if(data.updated > 0 || data.disabled > 0) {
fetchCurrentToken();
}
// 5秒后隐藏提示
setTimeout(() => {
checkResult.style.display = 'none';
}, 5000);
} else {
alert('检测失败: ' + (data.error || '未知错误'));
}
})
.catch(error => {
alert('请求失败: ' + error.message);
})
.finally(() => {
button.classList.remove('loading');
});
});
// 获取授权地址按钮事件
document.getElementById('get-auth-url').addEventListener('click', function() {
const button = this;
const btnText = button.querySelector('.btn-text');
const originalText = btnText.textContent;
// 显示加载状态
button.disabled = true;
btnText.textContent = '获取中...';
// 请求授权地址
fetch('/auth')
.then(response => response.json())
.then(data => {
if (data.authorize_url) {
// 显示授权地址
const authUrlElement = document.getElementById('auth-url');
authUrlElement.textContent = data.authorize_url;
authUrlElement.style.display = 'block';
} else {
alert('获取授权地址失败: ' + (data.error || '未知错误'));
}
})
.catch(error => {
alert('请求失败: ' + error.message);
})
.finally(() => {
// 恢复按钮状态
button.disabled = false;
btnText.textContent = originalText;
});
});
// 验证JSON格式
function isValidJSON(str) {
try {
JSON.parse(str);
return true;
} catch (e) {
return false;
}
}
// 验证授权响应格式
function validateAuthResponse(response) {
try {
const data = JSON.parse(response);
// 检查必要字段
if (!data.code || !data.state || !data.tenant_url) {
return { valid: false, message: '缺少必要字段 (code, state, tenant_url)' };
}
return { valid: true, data: data };
} catch (e) {
return { valid: false, message: 'JSON格式无效: ' + e.message };
}
}
// 提交授权响应按钮事件
document.getElementById('submit-auth').addEventListener('click', function() {
const button = this;
const btnText = button.querySelector('.btn-text');
const originalText = btnText.textContent;
const responseText = document.getElementById('auth-response').value.trim();
const validationMessage = document.getElementById('validation-message');
const submitResult = document.getElementById('submit-result');
// 重置消息
validationMessage.style.display = 'none';
submitResult.style.display = 'none';
// 验证输入
if (!responseText) {
validationMessage.textContent = '请输入授权响应';
validationMessage.style.display = 'block';
return;
}
// 验证JSON格式
if (!isValidJSON(responseText)) {
validationMessage.textContent = 'JSON格式无效,请检查输入';
validationMessage.style.display = 'block';
return;
}
// 验证授权响应格式
const validation = validateAuthResponse(responseText);
if (!validation.valid) {
validationMessage.textContent = validation.message;
validationMessage.style.display = 'block';
return;
}
// 显示加载状态
button.disabled = true;
btnText.textContent = '处理中...';
// 提交授权响应
fetch('/callback', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: responseText
})
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
// 显示成功消息
submitResult.textContent = 'Token获取成功!';
submitResult.style.display = 'block';
// 清空输入框
document.getElementById('auth-response').value = '';
// 刷新Token列表
setTimeout(() => {
fetchCurrentToken();
// 切换到Token列表面板
document.querySelector('.menu-item[data-target="token-list-panel"]').click();
}, 1000);
} else {
// 显示错误消息但不影响样式
validationMessage.textContent = '获取Token失败: ' + (data.error || '未知错误');
validationMessage.style.display = 'block';
}
})
.catch(error => {
// 显示错误消息但不影响样式
validationMessage.textContent = '请求失败: ' + error.message;
validationMessage.style.display = 'block';
})
.finally(() => {
// 恢复按钮状态
button.disabled = false;
btnText.textContent = originalText;
});
});
// 初始加载token列表
fetchCurrentToken();
// 添加Token隐藏/显示功能
let tokenVisible = true;
const toggleTokenVisibilityBtn = document.getElementById('toggle-token-visibility');
toggleTokenVisibilityBtn.addEventListener('click', function() {
tokenVisible = !tokenVisible;
// 更新按钮状态和文本
if (tokenVisible) {
this.innerHTML = '<i class="bi bi-eye-slash"></i> 隐藏Token';
this.classList.remove('active');
} else {
this.innerHTML = '<i class="bi bi-eye"></i> 显示Token';
this.classList.add('active');
}
// 应用模糊效果到所有Token展示区域
applyTokenVisibility();
});
// 应用Token可见性设置到列表中
function applyTokenVisibility() {
// 获取所有Token显示元素
const tokenElements = document.querySelectorAll('.token-summary, .token-display');
tokenElements.forEach(element => {
if (tokenVisible) {
element.classList.remove('token-blur');
} else {
element.classList.add('token-blur');
}
});
}
// 在渲染Token列表后应用可见性设置
const originalRenderTokenList = renderTokenList;
renderTokenList = function(totalItems, totalPages) {
originalRenderTokenList(totalItems, totalPages);
// 如果当前状态是隐藏的,则应用模糊效果
if (!tokenVisible) {
applyTokenVisibility();
}
};
});
</script>
</body>
</html>