fix: center login title and use full time unit names

This commit is contained in:
Quorinex
2026-02-04 02:06:28 +08:00
parent d62cb6b897
commit b27fd3528a

View File

@@ -2,35 +2,36 @@
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Kiro API Proxy</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, -apple-system, sans-serif; background: #f8fafc; color: #1e293b; min-height: 100vh; }
.container { max-width: 1100px; margin: 0 auto; padding: 20px; }
.login-wrapper { min-height: 100vh; display: flex; align-items: center; justify-content: center; background: linear-gradient(135deg, #f0f4ff 0%, #faf5ff 100%); }
.login-box { background: #fff; padding: 40px; border-radius: 16px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); width: 100%; max-width: 380px; }
.login-box h1 { text-align: center; color: #7c3aed; margin-bottom: 8px; }
body { font-family: system-ui, -apple-system, sans-serif; background: #f8fafc; color: #1e293b; min-height: 100vh; -webkit-tap-highlight-color: transparent; }
.container { max-width: 1100px; margin: 0 auto; padding: 16px; }
.login-wrapper { min-height: 100vh; display: flex; align-items: center; justify-content: center; background: linear-gradient(135deg, #f0f4ff 0%, #faf5ff 100%); padding: 16px; }
.login-box { background: #fff; padding: 32px 24px; border-radius: 16px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); width: 100%; max-width: 380px; }
.login-box h1 { text-align: center; color: #7c3aed; margin-bottom: 8px; font-size: 22px; justify-content: center; }
.login-box p { text-align: center; color: #64748b; margin-bottom: 24px; font-size: 14px; }
.form-group { margin-bottom: 16px; }
label { display: block; margin-bottom: 6px; color: #374151; font-size: 14px; font-weight: 500; }
input[type="text"], input[type="password"], select, textarea { width: 100%; padding: 10px 14px; border: 1px solid #e2e8f0; border-radius: 8px; font-size: 14px; background: #fff; color: #1e293b; }
input[type="text"], input[type="password"], select, textarea { width: 100%; padding: 12px 14px; border: 1px solid #e2e8f0; border-radius: 8px; font-size: 16px; background: #fff; color: #1e293b; -webkit-appearance: none; }
input:focus, textarea:focus { outline: none; border-color: #7c3aed; box-shadow: 0 0 0 3px rgba(124,58,237,0.1); }
textarea { resize: vertical; min-height: 80px; font-family: monospace; }
.btn { padding: 10px 18px; border: none; border-radius: 8px; cursor: pointer; font-size: 14px; font-weight: 500; transition: all 0.2s; }
textarea { resize: vertical; min-height: 100px; font-family: monospace; font-size: 14px; }
.btn { padding: 12px 18px; border: none; border-radius: 8px; cursor: pointer; font-size: 14px; font-weight: 500; transition: all 0.2s; touch-action: manipulation; }
.btn-primary { background: #7c3aed; color: white; }
.btn-primary:hover { background: #6d28d9; }
.btn-primary:hover, .btn-primary:active { background: #6d28d9; }
.btn-danger { background: #ef4444; color: white; }
.btn-secondary { background: #f1f5f9; color: #374151; }
.btn-sm { padding: 6px 12px; font-size: 13px; }
.card { background: #fff; border-radius: 12px; padding: 20px; margin-bottom: 16px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
.card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid #e2e8f0; }
.btn-sm { padding: 8px 12px; font-size: 13px; }
.card { background: #fff; border-radius: 12px; padding: 16px; margin-bottom: 16px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
.card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid #e2e8f0; flex-wrap: wrap; gap: 12px; }
.card-title { font-size: 16px; font-weight: 600; }
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 12px; margin-bottom: 20px; }
.stat-card { background: #fff; border-radius: 10px; padding: 16px; text-align: center; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
.stat-value { font-size: 24px; font-weight: 700; color: #7c3aed; }
.stat-label { font-size: 12px; color: #64748b; margin-top: 4px; }
.badge { display: inline-block; padding: 3px 8px; border-radius: 4px; font-size: 12px; font-weight: 500; }
.card-actions { display: flex; gap: 8px; flex-wrap: wrap; }
.stats-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-bottom: 20px; }
.stat-card { background: #fff; border-radius: 10px; padding: 12px 8px; text-align: center; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
.stat-value { font-size: 20px; font-weight: 700; color: #7c3aed; }
.stat-label { font-size: 11px; color: #64748b; margin-top: 2px; }
.badge { display: inline-block; padding: 3px 8px; border-radius: 4px; font-size: 11px; font-weight: 500; white-space: nowrap; }
.badge-success { background: #dcfce7; color: #16a34a; }
.badge-warning { background: #fef3c7; color: #d97706; }
.badge-error { background: #fee2e2; color: #dc2626; }
@@ -39,59 +40,93 @@
.badge-proplus { background: #8b5cf6; color: white; }
.badge-power { background: #f59e0b; color: white; }
.badge-free { background: #6b7280; color: white; }
.modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.4); z-index: 1000; }
.modal.active { display: flex; align-items: center; justify-content: center; }
.modal-content { background: #fff; border-radius: 12px; padding: 24px; max-width: 600px; width: 90%; max-height: 90vh; overflow-y: auto; }
.modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 1000; padding: 16px; overflow-y: auto; }
.modal.active { display: flex; align-items: flex-start; justify-content: center; padding-top: 5vh; }
.modal-content { background: #fff; border-radius: 12px; padding: 20px; width: 100%; max-width: 600px; margin-bottom: 20px; }
.modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
.modal-title { font-size: 18px; font-weight: 600; }
.modal-close { background: none; border: none; font-size: 24px; cursor: pointer; color: #64748b; }
.modal-close { background: none; border: none; font-size: 28px; cursor: pointer; color: #64748b; padding: 0 8px; }
.modal-footer { margin-top: 20px; display: flex; gap: 10px; justify-content: flex-end; }
.switch { position: relative; display: inline-block; width: 44px; height: 24px; }
.switch { position: relative; display: inline-block; width: 44px; height: 24px; flex-shrink: 0; }
.slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background: #cbd5e1; border-radius: 24px; transition: 0.3s; }
.slider:before { position: absolute; content: ""; height: 18px; width: 18px; left: 3px; bottom: 3px; background: white; border-radius: 50%; transition: 0.3s; }
input:checked + .slider { background: #7c3aed; }
input:checked + .slider:before { transform: translateX(20px); }
.tabs { display: flex; gap: 4px; margin-bottom: 20px; background: #f1f5f9; padding: 4px; border-radius: 10px; }
.tab { padding: 10px 18px; border-radius: 8px; cursor: pointer; color: #64748b; font-weight: 500; font-size: 14px; }
.tabs { display: flex; gap: 4px; margin-bottom: 16px; background: #f1f5f9; padding: 4px; border-radius: 10px; overflow-x: auto; -webkit-overflow-scrolling: touch; }
.tab { padding: 10px 16px; border-radius: 8px; cursor: pointer; color: #64748b; font-weight: 500; font-size: 14px; white-space: nowrap; flex-shrink: 0; }
.tab:hover { color: #374151; }
.tab.active { background: #fff; color: #7c3aed; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
.hidden { display: none !important; }
.message { padding: 12px 16px; border-radius: 8px; margin-bottom: 12px; font-size: 14px; }
.message-error { background: #fee2e2; color: #dc2626; }
.endpoint { background: #f8fafc; padding: 12px 16px; border-radius: 8px; font-family: monospace; font-size: 13px; margin-bottom: 10px; display: flex; justify-content: space-between; align-items: center; }
.header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px; }
.header h1 { font-size: 20px; color: #7c3aed; }
.account-card { background: #fff; border-radius: 12px; padding: 16px; margin-bottom: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); border-left: 4px solid #7c3aed; }
.account-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 12px; }
.account-email { font-weight: 600; font-size: 15px; color: #1e293b; }
.account-meta { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 4px; }
.account-usage { margin: 12px 0; }
.usage-bar { height: 8px; background: #e2e8f0; border-radius: 4px; overflow: hidden; }
.usage-fill { height: 100%; background: #7c3aed; border-radius: 4px; transition: width 0.3s; }
.endpoint { background: #f8fafc; padding: 12px; border-radius: 8px; font-family: monospace; font-size: 12px; margin-bottom: 10px; display: flex; justify-content: space-between; align-items: center; gap: 10px; word-break: break-all; }
.endpoint span { flex: 1; }
.header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; gap: 12px; }
.header h1 { font-size: 18px; color: #7c3aed; }
.account-card { background: #fff; border-radius: 12px; padding: 14px; margin-bottom: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); border-left: 4px solid #7c3aed; }
.account-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 10px; gap: 10px; }
.account-info { flex: 1; min-width: 0; }
.account-email { font-weight: 600; font-size: 14px; color: #1e293b; word-break: break-all; }
.account-meta { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 6px; }
.account-usage { margin: 10px 0; }
.usage-bar { height: 6px; background: #e2e8f0; border-radius: 3px; overflow: hidden; }
.usage-fill { height: 100%; background: #7c3aed; border-radius: 3px; transition: width 0.3s; }
.usage-fill.high { background: #f59e0b; }
.usage-fill.critical { background: #ef4444; }
.usage-text { display: flex; justify-content: space-between; font-size: 12px; color: #64748b; margin-top: 4px; }
.account-stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; margin-top: 12px; padding-top: 12px; border-top: 1px solid #e2e8f0; }
.usage-text { display: flex; justify-content: space-between; font-size: 11px; color: #64748b; margin-top: 4px; }
.account-stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; margin-top: 10px; padding-top: 10px; border-top: 1px solid #e2e8f0; }
.account-stat { text-align: center; }
.account-stat-value { font-weight: 600; font-size: 14px; }
.account-stat-label { font-size: 11px; color: #64748b; }
.account-actions { display: flex; gap: 6px; }
.account-stat-value { font-weight: 600; font-size: 13px; }
.account-stat-label { font-size: 10px; color: #64748b; }
.account-actions { display: flex; gap: 6px; flex-shrink: 0; flex-wrap: wrap; }
.detail-section { margin-bottom: 20px; }
.detail-section h4 { font-size: 14px; color: #64748b; margin-bottom: 10px; text-transform: uppercase; letter-spacing: 0.5px; }
.detail-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; }
.detail-item { background: #f8fafc; padding: 12px; border-radius: 8px; }
.detail-label { font-size: 12px; color: #64748b; margin-bottom: 4px; }
.detail-value { font-size: 14px; font-weight: 500; }
.model-list { display: grid; gap: 8px; max-height: 300px; overflow-y: auto; }
.model-item { background: #f8fafc; padding: 10px 12px; border-radius: 8px; display: flex; justify-content: space-between; align-items: center; }
.detail-section h4 { font-size: 13px; color: #64748b; margin-bottom: 10px; text-transform: uppercase; letter-spacing: 0.5px; }
.detail-grid { display: grid; grid-template-columns: 1fr; gap: 10px; }
.detail-item { background: #f8fafc; padding: 10px 12px; border-radius: 8px; }
.detail-label { font-size: 11px; color: #64748b; margin-bottom: 2px; }
.detail-value { font-size: 13px; font-weight: 500; word-break: break-all; }
.model-list { display: grid; gap: 8px; max-height: 250px; overflow-y: auto; }
.model-item { background: #f8fafc; padding: 10px 12px; border-radius: 8px; }
.model-name { font-weight: 500; font-size: 13px; }
.model-info { font-size: 11px; color: #64748b; }
.btn-icon { display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; padding: 0; }
.btn-icon svg { width: 16px; height: 16px; }
.icon { width: 16px; height: 16px; vertical-align: middle; }
.model-info { font-size: 11px; color: #64748b; margin-top: 2px; }
.btn-icon { display: inline-flex; align-items: center; justify-content: center; width: 36px; height: 36px; padding: 0; }
.btn-icon svg { width: 18px; height: 18px; }
.loading { opacity: 0.6; pointer-events: none; }
.logo { display: flex; align-items: center; gap: 8px; }
.logo svg { width: 24px; height: 24px; color: #7c3aed; }
.machine-id-row { display: flex; gap: 8px; align-items: stretch; flex-wrap: wrap; }
.machine-id-row input { flex: 1; min-width: 200px; font-family: monospace; font-size: 11px; }
.machine-id-row .btn { flex-shrink: 0; }
/* 移动端适配 */
@media (max-width: 640px) {
.container { padding: 12px; }
.stats-grid { grid-template-columns: repeat(3, 1fr); gap: 8px; }
.stat-card { padding: 10px 6px; }
.stat-value { font-size: 18px; }
.stat-label { font-size: 10px; }
.card { padding: 14px; }
.card-header { flex-direction: column; align-items: flex-start; }
.card-actions { width: 100%; }
.card-actions .btn { flex: 1; text-align: center; }
.account-header { flex-direction: column; }
.account-actions { width: 100%; justify-content: flex-start; }
.account-actions .btn { padding: 8px 10px; font-size: 12px; }
.account-stats { grid-template-columns: repeat(2, 1fr); }
.detail-grid { grid-template-columns: 1fr; }
.tabs { gap: 2px; }
.tab { padding: 10px 12px; font-size: 13px; }
.modal-content { padding: 16px; }
.endpoint { flex-direction: column; align-items: stretch; }
.endpoint .btn { width: 100%; }
.machine-id-row { flex-direction: column; }
.machine-id-row input { min-width: 100%; }
.machine-id-row .btn { width: 100%; }
}
@media (min-width: 641px) {
.detail-grid { grid-template-columns: repeat(2, 1fr); }
}
</style>
</head>
<body>
@@ -124,19 +159,19 @@
</div>
<div class="tabs">
<div class="tab active" data-tab="accounts">账号管理</div>
<div class="tab active" data-tab="accounts">账号</div>
<div class="tab" data-tab="settings">设置</div>
<div class="tab" data-tab="api">API 端点</div>
<div class="tab" data-tab="api">API</div>
</div>
<div id="tabAccounts" class="tab-content">
<div class="card">
<div class="card-header">
<span class="card-title">账号列表</span>
<div>
<div class="card-actions">
<button class="btn btn-secondary btn-sm" onclick="showModal('credentials')">导入凭证</button>
<button class="btn btn-secondary btn-sm" onclick="showModal('sso')">SSO Token</button>
<button class="btn btn-primary btn-sm" onclick="showModal('iam')">IAM SSO</button>
<button class="btn btn-secondary btn-sm" onclick="showModal('sso')">SSO</button>
<button class="btn btn-primary btn-sm" onclick="showModal('iam')">IAM</button>
</div>
</div>
<div id="accountsList"></div>
@@ -175,11 +210,11 @@
<div id="tabApi" class="tab-content hidden">
<div class="card">
<div class="card-header"><span class="card-title">API 端点</span></div>
<p style="margin-bottom:12px;font-weight:500">Claude API</p>
<p style="margin-bottom:10px;font-weight:500;font-size:14px">Claude API</p>
<div class="endpoint"><span id="claudeEndpoint"></span><button class="btn btn-sm btn-secondary" onclick="copy('claudeEndpoint')">复制</button></div>
<p style="margin:16px 0 12px;font-weight:500">OpenAI API</p>
<p style="margin:14px 0 10px;font-weight:500;font-size:14px">OpenAI API</p>
<div class="endpoint"><span id="openaiEndpoint"></span><button class="btn btn-sm btn-secondary" onclick="copy('openaiEndpoint')">复制</button></div>
<p style="margin:16px 0 12px;font-weight:500">模型列表</p>
<p style="margin:14px 0 10px;font-weight:500;font-size:14px">模型列表</p>
<div class="endpoint"><span id="modelsEndpoint"></span><button class="btn btn-sm btn-secondary" onclick="copy('modelsEndpoint')">复制</button></div>
</div>
</div>
@@ -196,7 +231,7 @@
</div>
<div id="detailModal" class="modal">
<div class="modal-content" style="max-width:700px">
<div class="modal-content">
<div class="modal-header">
<span class="modal-title">账号详情</span>
<button class="modal-close" onclick="closeDetailModal()">&times;</button>
@@ -260,7 +295,7 @@
document.getElementById('statSuccess').textContent = d.successRequests || 0;
document.getElementById('statFailed').textContent = d.failedRequests || 0;
document.getElementById('statTokens').textContent = formatNum(d.totalTokens || 0);
document.getElementById('statCredits').textContent = (d.totalCredits || 0).toFixed(2);
document.getElementById('statCredits').textContent = (d.totalCredits || 0).toFixed(1);
}
async function loadAccounts() {
@@ -272,7 +307,7 @@
function renderAccounts() {
const container = document.getElementById('accountsList');
if (accountsData.length === 0) {
container.innerHTML = '<p style="text-align:center;color:#64748b;padding:40px">暂无账号,请添加</p>';
container.innerHTML = '<p style="text-align:center;color:#64748b;padding:30px">暂无账号</p>';
return;
}
container.innerHTML = accountsData.map(a => {
@@ -280,7 +315,7 @@
const usageClass = usagePercent > 90 ? 'critical' : usagePercent > 70 ? 'high' : '';
return `<div class="account-card">
<div class="account-header">
<div>
<div class="account-info">
<div class="account-email">${a.email || a.id.substring(0,12)+'...'}</div>
<div class="account-meta">
${getSubBadge(a.subscriptionType)}
@@ -289,8 +324,8 @@
</div>
</div>
<div class="account-actions">
<button class="btn btn-sm btn-icon btn-secondary" onclick="refreshAccount('${a.id}')" title="刷新信息"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 4v6h-6M1 20v-6h6"/><path d="M3.51 9a9 9 0 0114.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0020.49 15"/></svg></button>
<button class="btn btn-sm btn-icon btn-secondary" onclick="showDetail('${a.id}')" title="详情"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><path d="M14 2v6h6M16 13H8M16 17H8M10 9H8"/></svg></button>
<button class="btn btn-sm btn-icon btn-secondary" onclick="refreshAccount('${a.id}')" title="刷新"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 4v6h-6M1 20v-6h6"/><path d="M3.51 9a9 9 0 0114.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0020.49 15"/></svg></button>
<button class="btn btn-sm btn-icon btn-secondary" onclick="showDetail('${a.id}')" title="详情"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4M12 8h.01"/></svg></button>
<button class="btn btn-sm ${a.enabled ? 'btn-secondary' : 'btn-primary'}" onclick="toggleAccount('${a.id}',${!a.enabled})">${a.enabled ? '禁用' : '启用'}</button>
<button class="btn btn-sm btn-danger" onclick="deleteAccount('${a.id}')">删除</button>
</div>
@@ -299,14 +334,14 @@
<div class="usage-bar"><div class="usage-fill ${usageClass}" style="width:${usagePercent}%"></div></div>
<div class="usage-text">
<span>${a.usageCurrent?.toFixed(1) || 0} / ${a.usageLimit?.toFixed(0) || 0}</span>
<span>${usagePercent.toFixed(1)}%${a.nextResetDate ? ' · 重置: '+a.nextResetDate : ''}</span>
<span>${usagePercent.toFixed(1)}%</span>
</div>
</div>` : ''}
<div class="account-stats">
<div class="account-stat"><div class="account-stat-value">${a.requestCount || 0}</div><div class="account-stat-label">请求</div></div>
<div class="account-stat"><div class="account-stat-value">${formatNum(a.totalTokens || 0)}</div><div class="account-stat-label">Tokens</div></div>
<div class="account-stat"><div class="account-stat-value">${(a.totalCredits || 0).toFixed(2)}</div><div class="account-stat-label">Credits</div></div>
<div class="account-stat"><div class="account-stat-value">${formatTokenExpiry(a.expiresAt)}</div><div class="account-stat-label">Token到期</div></div>
<div class="account-stat"><div class="account-stat-value">${(a.totalCredits || 0).toFixed(1)}</div><div class="account-stat-label">Credits</div></div>
<div class="account-stat"><div class="account-stat-value">${formatTokenExpiry(a.expiresAt)}</div><div class="account-stat-label">到期</div></div>
</div>
</div>`;
}).join('');
@@ -332,7 +367,7 @@
const diff = ts - Date.now()/1000;
if (diff <= 0) return '已过期';
if (diff < 3600) return Math.floor(diff/60) + '分钟';
if (diff < 86400) return Math.floor(diff/3600) + '时';
if (diff < 86400) return Math.floor(diff/3600) + '时';
return Math.floor(diff/86400) + '天';
}
@@ -350,21 +385,20 @@
async function showDetail(id) {
const a = accountsData.find(x => x.id === id);
if (!a) return;
const body = document.getElementById('detailBody');
body.innerHTML = `
document.getElementById('detailBody').innerHTML = `
<div class="detail-section">
<h4>基本信息</h4>
<div class="detail-grid">
<div class="detail-item"><div class="detail-label">邮箱</div><div class="detail-value">${a.email || '-'}</div></div>
<div class="detail-item"><div class="detail-label">用户ID</div><div class="detail-value" style="font-size:12px;word-break:break-all">${a.userId || '-'}</div></div>
<div class="detail-item"><div class="detail-label">用户ID</div><div class="detail-value">${a.userId || '-'}</div></div>
<div class="detail-item"><div class="detail-label">认证方式</div><div class="detail-value">${a.authMethod || '-'}</div></div>
<div class="detail-item"><div class="detail-label">Region</div><div class="detail-value">${a.region || 'us-east-1'}</div></div>
</div>
</div>
<div class="detail-section">
<h4>机器码</h4>
<div style="display:flex;gap:8px;align-items:center">
<input type="text" id="machineIdInput" value="${a.machineId || ''}" style="flex:1;font-family:monospace;font-size:12px" placeholder="UUID 格式: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx">
<div class="machine-id-row">
<input type="text" id="machineIdInput" value="${a.machineId || ''}" placeholder="UUID格式">
<button class="btn btn-sm btn-secondary" onclick="generateMachineId()">生成</button>
<button class="btn btn-sm btn-primary" onclick="saveMachineId('${id}')">保存</button>
</div>
@@ -373,17 +407,8 @@
<h4>订阅信息</h4>
<div class="detail-grid">
<div class="detail-item"><div class="detail-label">订阅类型</div><div class="detail-value">${a.subscriptionTitle || a.subscriptionType || '-'}</div></div>
<div class="detail-item"><div class="detail-label">剩余天数</div><div class="detail-value">${a.daysRemaining || '-'}</div></div>
<div class="detail-item"><div class="detail-label">Token到期</div><div class="detail-value">${a.expiresAt ? new Date(a.expiresAt*1000).toLocaleString() : '-'}</div></div>
<div class="detail-item"><div class="detail-label">上次刷新</div><div class="detail-value">${a.lastRefresh ? new Date(a.lastRefresh*1000).toLocaleString() : '-'}</div></div>
</div>
</div>
<div class="detail-section">
<h4>使用量</h4>
<div class="detail-grid">
<div class="detail-item"><div class="detail-label">当前用量</div><div class="detail-value">${a.usageCurrent?.toFixed(2) || 0}</div></div>
<div class="detail-item"><div class="detail-label">用量上限</div><div class="detail-value">${a.usageLimit?.toFixed(0) || 0}</div></div>
<div class="detail-item"><div class="detail-label">使用比例</div><div class="detail-value">${((a.usagePercent||0)*100).toFixed(1)}%</div></div>
<div class="detail-item"><div class="detail-label">用量</div><div class="detail-value">${a.usageCurrent?.toFixed(1) || 0} / ${a.usageLimit?.toFixed(0) || 0}</div></div>
<div class="detail-item"><div class="detail-label">重置日期</div><div class="detail-value">${a.nextResetDate || '-'}</div></div>
</div>
</div>
@@ -397,10 +422,9 @@
</div>
</div>
<div class="detail-section">
<h4>可用模型 <button class="btn btn-sm btn-secondary" onclick="loadModels('${id}')" style="margin-left:10px">加载</button></h4>
<div id="modelsList" class="model-list"><p style="color:#64748b;font-size:13px">点击加载按钮获取模型列表</p></div>
</div>
`;
<h4>可用模型 <button class="btn btn-sm btn-secondary" onclick="loadModels('${id}')" style="margin-left:8px">加载</button></h4>
<div id="modelsList" class="model-list"><p style="color:#64748b;font-size:12px">点击加载按钮获取</p></div>
</div>`;
document.getElementById('detailModal').classList.add('active');
}
@@ -411,10 +435,7 @@
const res = await fetch('/admin/api/accounts/' + id + '/models', { headers: { 'X-Admin-Password': password } });
const d = await res.json();
if (d.success && d.models) {
container.innerHTML = d.models.map(m => `<div class="model-item">
<div><div class="model-name">${m.modelId}</div><div class="model-info">${m.description || ''}</div></div>
<div class="model-info">${m.tokenLimits ? (m.tokenLimits.maxInputTokens/1000)+'K / '+(m.tokenLimits.maxOutputTokens/1000)+'K' : ''}</div>
</div>`).join('') || '<p style="color:#64748b">无可用模型</p>';
container.innerHTML = d.models.map(m => `<div class="model-item"><div class="model-name">${m.modelId}</div><div class="model-info">${m.description || ''}</div></div>`).join('') || '<p style="color:#64748b">无可用模型</p>';
} else {
container.innerHTML = '<p style="color:#ef4444">加载失败: ' + (d.error || '未知错误') + '</p>';
}
@@ -427,32 +448,22 @@
try {
const res = await fetch('/admin/api/generate-machine-id', { headers: { 'X-Admin-Password': password } });
const d = await res.json();
if (d.machineId) {
document.getElementById('machineIdInput').value = d.machineId;
}
if (d.machineId) document.getElementById('machineIdInput').value = d.machineId;
} catch (e) { alert('生成失败'); }
}
async function saveMachineId(id) {
const machineId = document.getElementById('machineIdInput').value.trim();
// UUID 格式或 32位十六进制
if (machineId && !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(machineId) && !/^[0-9a-f]{32}$/i.test(machineId)) {
alert('机器码格式错误,需要 UUID 格式 (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx) 或 32位十六进制');
return;
alert('机器码格式错误'); return;
}
try {
const res = await fetch('/admin/api/accounts/' + id, {
method: 'PUT',
headers: { 'Content-Type': 'application/json', 'X-Admin-Password': password },
method: 'PUT', headers: { 'Content-Type': 'application/json', 'X-Admin-Password': password },
body: JSON.stringify({ machineId })
});
const d = await res.json();
if (d.success) {
alert('已保存');
loadAccounts();
} else {
alert('保存失败: ' + d.error);
}
if (d.success) { alert('已保存'); loadAccounts(); } else { alert('保存失败: ' + d.error); }
} catch (e) { alert('保存失败'); }
}
@@ -465,8 +476,7 @@
async function saveSettings() {
await fetch('/admin/api/settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Admin-Password': password },
method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Admin-Password': password },
body: JSON.stringify({ requireApiKey: document.getElementById('requireApiKey').checked, apiKey: document.getElementById('apiKeyInput').value })
});
alert('已保存');
@@ -476,8 +486,7 @@
const newPwd = document.getElementById('newPassword').value;
if (!newPwd) return alert('请输入新密码');
await fetch('/admin/api/settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Admin-Password': password },
method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Admin-Password': password },
body: JSON.stringify({ password: newPwd })
});
password = newPwd;
@@ -500,8 +509,7 @@
async function toggleAccount(id, enabled) {
await fetch('/admin/api/accounts/' + id, {
method: 'PUT',
headers: { 'Content-Type': 'application/json', 'X-Admin-Password': password },
method: 'PUT', headers: { 'Content-Type': 'application/json', 'X-Admin-Password': password },
body: JSON.stringify({ enabled })
});
loadAccounts();
@@ -532,7 +540,7 @@
if (type === 'credentials') {
title.textContent = '导入凭证';
body.innerHTML = `
<div class="form-group"><label>凭证 JSON</label><textarea id="credJson" placeholder='{"accessToken":"...","refreshToken":"...","clientId":"...","clientSecret":"..."}'></textarea></div>
<div class="form-group"><label>凭证 JSON</label><textarea id="credJson" placeholder='{"refreshToken":"...","clientId":"...","clientSecret":"..."}'></textarea></div>
<div class="form-group"><label>认证方式</label><select id="credAuth"><option value="social">Social</option><option value="idc">IAM IdC</option></select></div>
<div class="modal-footer"><button class="btn btn-secondary" onclick="closeModal()">取消</button><button class="btn btn-primary" onclick="importCredentials()">导入</button></div>`;
} else if (type === 'sso') {
@@ -547,7 +555,7 @@
<div class="form-group"><label>Start URL</label><input type="text" id="iamStartUrl" placeholder="https://xxx.awsapps.com/start"></div>
<div class="form-group"><label>Region</label><input type="text" id="iamRegion" value="us-east-1"></div>
<div id="iamStep2" class="hidden">
<p style="color:#16a34a;margin:12px 0">请在浏览器中完成登录,然后粘贴回调 URL</p>
<p style="color:#16a34a;margin:12px 0;font-size:14px">请在浏览器中完成登录,然后粘贴回调 URL</p>
<div class="form-group"><label>回调 URL</label><input type="text" id="iamCallback" placeholder="http://127.0.0.1:xxx/?code=..."></div>
</div>
<div class="modal-footer"><button class="btn btn-secondary" onclick="closeModal()">取消</button><button class="btn btn-primary" id="iamBtn" onclick="startIamSso()">开始登录</button></div>`;
@@ -555,15 +563,14 @@
modal.classList.add('active');
}
function closeModal() { document.getElementById('addModal').classList.remove('active'); }
function closeModal() { document.getElementById('addModal').classList.remove('active'); iamSession = ''; }
async function importCredentials() {
try {
const json = JSON.parse(document.getElementById('credJson').value);
json.authMethod = document.getElementById('credAuth').value;
const res = await fetch('/admin/api/auth/credentials', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Admin-Password': password },
method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Admin-Password': password },
body: JSON.stringify(json)
});
const d = await res.json();
@@ -574,8 +581,7 @@
async function importSsoToken() {
const res = await fetch('/admin/api/auth/sso-token', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Admin-Password': password },
method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Admin-Password': password },
body: JSON.stringify({ bearerToken: document.getElementById('ssoToken').value, region: document.getElementById('ssoRegion').value })
});
const d = await res.json();
@@ -587,17 +593,15 @@
async function startIamSso() {
if (iamSession) {
const res = await fetch('/admin/api/auth/iam-sso/complete', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Admin-Password': password },
method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Admin-Password': password },
body: JSON.stringify({ sessionId: iamSession, callbackUrl: document.getElementById('iamCallback').value })
});
const d = await res.json();
if (d.success) { closeModal(); loadAccounts(); loadStats(); alert('登录成功: ' + (d.account?.email || d.account?.id)); iamSession = ''; }
if (d.success) { closeModal(); loadAccounts(); loadStats(); alert('登录成功: ' + (d.account?.email || d.account?.id)); }
else alert('失败: ' + d.error);
} else {
const res = await fetch('/admin/api/auth/iam-sso/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Admin-Password': password },
method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Admin-Password': password },
body: JSON.stringify({ startUrl: document.getElementById('iamStartUrl').value, region: document.getElementById('iamRegion').value })
});
const d = await res.json();
@@ -613,4 +617,4 @@
setInterval(() => { if (!document.getElementById('mainPage').classList.contains('hidden')) loadStats(); }, 10000);
</script>
</body>
</html>
</html>