Files
one-api-workers/public/index.html
T
dreamhunter2333andClaude Opus 4.5 a02ec82a8f feat: 现代化管理面板界面
- 添加 CSS 变量系统支持主题定制
- 改进视觉效果:更柔和的渐变、阴影和圆角
- 添加平滑的动画和过渡效果
- 优化卡片、按钮、表单等组件样式
- 改进响应式设计适配
- 美化滚动条和选择效果

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-11 13:19:33 +08:00

3097 lines
117 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AWSL One API 管理面板</title>
<link rel="icon"
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🤖</text></svg>">
<style>
/* === CSS Custom Properties === */
:root {
--primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--primary-color: #6366f1;
--primary-dark: #4f46e5;
--primary-light: #818cf8;
--secondary-color: #8b5cf6;
--accent-gradient: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
--success-color: #10b981;
--success-light: #d1fae5;
--warning-color: #f59e0b;
--warning-light: #fef3c7;
--danger-color: #ef4444;
--danger-light: #fee2e2;
--text-primary: #1e293b;
--text-secondary: #64748b;
--text-muted: #94a3b8;
--bg-primary: #f8fafc;
--bg-secondary: #f1f5f9;
--bg-card: rgba(255, 255, 255, 0.9);
--border-color: #e2e8f0;
--border-light: #f1f5f9;
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
--shadow-glow: 0 0 40px rgba(99, 102, 241, 0.15);
--radius-sm: 8px;
--radius-md: 12px;
--radius-lg: 16px;
--radius-xl: 24px;
--transition-fast: 0.15s ease;
--transition-normal: 0.25s ease;
--transition-slow: 0.4s ease;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: var(--primary-gradient);
background-attachment: fixed;
min-height: 100vh;
padding: 24px;
line-height: 1.6;
}
/* === Container === */
.container {
max-width: 1280px;
margin: 0 auto;
background: white;
border-radius: var(--radius-xl);
box-shadow: var(--shadow-xl), var(--shadow-glow);
overflow: hidden;
}
/* === Header === */
.header {
background: var(--accent-gradient);
color: white;
padding: 28px 32px;
text-align: center;
position: relative;
overflow: hidden;
}
.header::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23ffffff' fill-opacity='0.05'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
opacity: 0.5;
}
.header.compact {
padding: 18px 24px;
}
.header h1 {
font-size: 2rem;
font-weight: 700;
margin-bottom: 8px;
position: relative;
letter-spacing: -0.025em;
}
.header.compact h1 {
font-size: 1.5rem;
margin-bottom: 4px;
}
.header p {
margin-bottom: 0;
opacity: 0.9;
font-weight: 400;
position: relative;
}
.header.compact p {
font-size: 0.875rem;
}
.header-info {
position: absolute;
top: 18px;
left: 24px;
font-size: 13px;
opacity: 0.85;
z-index: 1;
}
.header-info a {
color: white;
text-decoration: none;
margin-right: 16px;
transition: opacity var(--transition-fast);
font-weight: 500;
}
.header-info a:hover {
opacity: 1;
text-decoration: none;
}
.header-info span {
background: rgba(255, 255, 255, 0.2);
padding: 4px 10px;
border-radius: 20px;
font-size: 12px;
}
/* === Auth Section === */
.auth-section {
padding: 80px 24px;
background: var(--bg-secondary);
text-align: center;
}
.auth-container {
max-width: 440px;
margin: 0 auto;
background: white;
padding: 48px 40px;
border-radius: var(--radius-lg);
box-shadow: var(--shadow-xl);
border: 1px solid var(--border-light);
}
.auth-title {
font-size: 1.75rem;
font-weight: 700;
margin-bottom: 32px;
color: var(--text-primary);
letter-spacing: -0.025em;
}
.auth-input {
width: 100%;
padding: 16px 18px;
border: 2px solid var(--border-color);
border-radius: var(--radius-md);
margin-bottom: 20px;
font-size: 15px;
transition: all var(--transition-normal);
background: var(--bg-primary);
}
.auth-input:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.1);
background: white;
}
.auth-btn {
width: 100%;
padding: 16px;
background: var(--accent-gradient);
color: white;
border: none;
border-radius: var(--radius-md);
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: all var(--transition-normal);
position: relative;
overflow: hidden;
}
.auth-btn::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
transition: left 0.5s ease;
}
.auth-btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(99, 102, 241, 0.35);
}
.auth-btn:hover::before {
left: 100%;
}
.auth-btn:active {
transform: translateY(0);
}
/* === Tabs === */
.tabs {
display: flex;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
padding: 4px;
gap: 4px;
}
.tab {
flex: 1;
padding: 14px 20px;
text-align: center;
cursor: pointer;
border: none;
background: transparent;
font-size: 14px;
font-weight: 500;
transition: all var(--transition-normal);
border-radius: var(--radius-sm);
color: var(--text-secondary);
position: relative;
}
.tab.active {
background: white;
color: var(--primary-color);
font-weight: 600;
box-shadow: var(--shadow-sm);
}
.tab:hover:not(.active) {
background: rgba(99, 102, 241, 0.05);
color: var(--primary-color);
}
.tab-content {
display: none;
padding: 32px;
animation: fadeIn 0.3s ease;
}
.tab-content.active {
display: block;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
/* === Buttons === */
.btn {
background: var(--accent-gradient);
color: white;
border: none;
padding: 10px 18px;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 13px;
font-weight: 600;
transition: all var(--transition-normal);
margin: 4px;
display: inline-flex;
align-items: center;
gap: 6px;
position: relative;
overflow: hidden;
}
.btn::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
transform: translate(-50%, -50%);
transition: width 0.4s ease, height 0.4s ease;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(99, 102, 241, 0.35);
}
.btn:hover::before {
width: 200%;
height: 200%;
}
.btn:active {
transform: translateY(0);
}
.btn-secondary {
background: transparent;
color: var(--primary-color);
border: 2px solid var(--primary-color);
}
.btn-secondary:hover {
background: rgba(99, 102, 241, 0.08);
box-shadow: 0 4px 15px rgba(99, 102, 241, 0.15);
}
.btn-secondary.btn-warning {
background: transparent;
color: var(--warning-color);
border: 2px solid var(--warning-color);
}
.btn-secondary.btn-warning:hover {
background: rgba(245, 158, 11, 0.08);
box-shadow: 0 4px 15px rgba(245, 158, 11, 0.2);
}
.btn-danger {
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
}
.btn-danger:hover {
box-shadow: 0 6px 20px rgba(239, 68, 68, 0.35);
}
.btn-warning {
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
}
.btn-warning:hover {
box-shadow: 0 6px 20px rgba(245, 158, 11, 0.35);
}
/* === Form Elements === */
.form-group {
margin-bottom: 24px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: var(--text-primary);
font-size: 14px;
}
.form-control {
width: 100%;
padding: 12px 16px;
border: 2px solid var(--border-color);
border-radius: var(--radius-sm);
font-size: 14px;
transition: all var(--transition-normal);
background: var(--bg-primary);
}
.form-control:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.1);
background: white;
}
select.form-control {
cursor: pointer;
background-color: var(--bg-primary);
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%2364748b' d='M6 8L1 3h10z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 12px center;
padding-right: 36px;
appearance: none;
}
textarea.form-control {
resize: vertical;
min-height: 100px;
}
/* === Cards === */
.card {
background: white;
border: 1px solid var(--border-light);
border-radius: var(--radius-lg);
padding: 24px;
margin-bottom: 24px;
box-shadow: var(--shadow-md);
transition: all var(--transition-normal);
}
.card:hover {
box-shadow: var(--shadow-lg);
}
.card-header {
border-bottom: 1px solid var(--border-light);
padding-bottom: 16px;
margin-bottom: 24px;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 12px;
}
.card-header div {
display: flex;
gap: 8px;
align-items: center;
flex-wrap: wrap;
}
.card-title {
font-size: 1.25rem;
font-weight: 700;
color: var(--text-primary);
letter-spacing: -0.025em;
}
/* === Item List === */
.item-list {
max-height: 480px;
overflow-y: auto;
padding-right: 4px;
}
.item-list::-webkit-scrollbar {
width: 6px;
}
.item-list::-webkit-scrollbar-track {
background: var(--bg-secondary);
border-radius: 3px;
}
.item-list::-webkit-scrollbar-thumb {
background: var(--border-color);
border-radius: 3px;
}
.item-list::-webkit-scrollbar-thumb:hover {
background: var(--text-muted);
}
.item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 18px 20px;
border: 1px solid var(--border-light);
border-radius: var(--radius-md);
margin-bottom: 12px;
background: var(--bg-primary);
transition: all var(--transition-normal);
}
.item:hover {
border-color: var(--primary-light);
background: white;
box-shadow: var(--shadow-sm);
transform: translateX(4px);
}
.item-info {
flex: 1;
min-width: 0;
}
.item-key {
font-weight: 600;
color: var(--text-primary);
margin-bottom: 6px;
font-size: 15px;
}
.item-details {
font-size: 13px;
color: var(--text-secondary);
line-height: 1.5;
}
.actions {
display: flex;
gap: 8px;
flex-shrink: 0;
}
.copy-btn {
background: none;
border: none;
color: var(--primary-color);
cursor: pointer;
font-size: 12px;
margin-left: 5px;
padding: 4px 8px;
border-radius: var(--radius-sm);
transition: all var(--transition-fast);
}
.copy-btn:hover {
background-color: rgba(99, 102, 241, 0.1);
}
#tokenKeyActions {
display: flex;
gap: 8px;
}
#tokenKeyActions .btn {
white-space: nowrap;
min-width: 80px;
flex-shrink: 0;
}
/* === Alerts === */
.alert {
padding: 16px 20px;
border-radius: var(--radius-md);
margin-bottom: 20px;
font-weight: 500;
display: flex;
align-items: center;
gap: 12px;
}
.alert.alert-top {
position: fixed;
top: 24px;
left: 50%;
transform: translateX(-50%);
z-index: 9999;
max-width: 480px;
min-width: 320px;
margin-bottom: 0;
box-shadow: var(--shadow-xl);
animation: slideInDown 0.4s cubic-bezier(0.16, 1, 0.3, 1);
text-align: left;
backdrop-filter: blur(10px);
}
@keyframes slideInDown {
from {
opacity: 0;
transform: translateX(-50%) translateY(-30px) scale(0.95);
}
to {
opacity: 1;
transform: translateX(-50%) translateY(0) scale(1);
}
}
.alert-success {
background: linear-gradient(135deg, rgba(16, 185, 129, 0.1) 0%, rgba(16, 185, 129, 0.05) 100%);
color: #065f46;
border: 1px solid rgba(16, 185, 129, 0.3);
}
.alert-error {
background: linear-gradient(135deg, rgba(239, 68, 68, 0.1) 0%, rgba(239, 68, 68, 0.05) 100%);
color: #991b1b;
border: 1px solid rgba(239, 68, 68, 0.3);
}
.loading {
text-align: center;
padding: 48px;
color: var(--text-secondary);
}
/* === Loading Overlay === */
#loadingOverlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(15, 23, 42, 0.6);
backdrop-filter: blur(8px);
z-index: 10002;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: white;
opacity: 0;
transition: opacity var(--transition-normal);
pointer-events: auto;
cursor: wait;
}
#mainContent.loading-active {
position: relative;
pointer-events: none;
}
#mainContent.loading-active #loadingOverlay {
pointer-events: auto;
}
#loadingOverlay .spinner {
width: 48px;
height: 48px;
border: 3px solid rgba(255, 255, 255, 0.2);
border-top-color: white;
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin-bottom: 16px;
}
#loadingOverlay p {
margin-top: 8px;
font-size: 15px;
font-weight: 500;
opacity: 0.9;
}
.spinner {
border: 3px solid var(--border-color);
border-top: 3px solid var(--primary-color);
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 0.8s linear infinite;
margin: 0 auto 16px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
/* === Grid === */
.grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 24px;
}
/* === Pricing Table === */
.pricing-table {
overflow-x: auto;
border-radius: var(--radius-md);
}
.pricing-table table {
min-width: 600px;
border-collapse: separate;
border-spacing: 0;
}
.pricing-table thead tr {
background: var(--bg-secondary);
}
.pricing-table th {
padding: 14px 16px;
text-align: left;
font-weight: 600;
color: var(--text-primary);
border-bottom: 2px solid var(--border-color);
font-size: 13px;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.pricing-table td {
padding: 12px 16px;
border-bottom: 1px solid var(--border-light);
}
.pricing-table input[type="text"],
.pricing-table input[type="number"] {
width: 100%;
border: none;
background: transparent;
padding: 10px 12px;
font-size: 14px;
border-radius: var(--radius-sm);
transition: all var(--transition-fast);
}
.pricing-table input[type="text"]:focus,
.pricing-table input[type="number"]:focus {
outline: none;
background: white;
box-shadow: 0 0 0 2px var(--primary-color);
}
.pricing-table tr:hover td {
background-color: var(--bg-primary);
}
/* === Modal === */
.modal {
display: none;
position: fixed;
z-index: 10000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(15, 23, 42, 0.6);
backdrop-filter: blur(8px);
}
.modal-content {
background-color: white;
margin: 8% auto;
padding: 0;
border-radius: var(--radius-lg);
width: 90%;
max-width: 480px;
box-shadow: var(--shadow-xl), 0 0 60px rgba(99, 102, 241, 0.15);
animation: modalSlideIn 0.4s cubic-bezier(0.16, 1, 0.3, 1);
overflow: hidden;
}
@keyframes modalSlideIn {
from {
opacity: 0;
transform: translateY(-40px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.modal-header {
background: var(--accent-gradient);
color: white;
padding: 24px 28px;
position: relative;
}
.modal-title {
font-size: 1.5rem;
font-weight: 700;
margin: 0;
letter-spacing: -0.025em;
}
.modal-close {
position: absolute;
top: 50%;
right: 20px;
transform: translateY(-50%);
background: rgba(255, 255, 255, 0.15);
border: none;
color: white;
width: 36px;
height: 36px;
border-radius: 50%;
font-size: 20px;
cursor: pointer;
transition: all var(--transition-fast);
display: flex;
align-items: center;
justify-content: center;
}
.modal-close:hover {
background: rgba(255, 255, 255, 0.25);
transform: translateY(-50%) scale(1.1);
}
.modal-body {
padding: 32px 28px;
}
/* === Feature Cards on Home === */
.feature-card {
border-radius: var(--radius-md);
padding: 24px;
transition: all var(--transition-normal);
position: relative;
overflow: hidden;
}
.feature-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, rgba(255,255,255,0.1) 0%, transparent 100%);
opacity: 0;
transition: opacity var(--transition-normal);
}
.feature-card:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-lg);
}
.feature-card:hover::before {
opacity: 1;
}
/* === Quick Start Section === */
.quick-start {
background: linear-gradient(135deg, var(--bg-primary) 0%, var(--bg-secondary) 100%);
padding: 24px;
border-radius: var(--radius-md);
border-left: 4px solid var(--primary-color);
}
.quick-start h4 {
color: var(--text-primary);
font-size: 16px;
font-weight: 700;
margin-bottom: 16px;
}
.quick-start ol {
margin: 0;
padding-left: 20px;
color: var(--text-secondary);
font-size: 14px;
}
.quick-start li {
margin-bottom: 10px;
line-height: 1.6;
}
.quick-start li:last-child {
margin-bottom: 0;
}
/* === Responsive === */
@media (max-width: 768px) {
body {
padding: 12px;
}
.container {
border-radius: var(--radius-lg);
}
.grid {
grid-template-columns: 1fr;
}
.tabs {
flex-direction: column;
padding: 8px;
}
.tab {
padding: 12px 16px;
}
.tab-content {
padding: 20px;
}
.item {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.actions {
width: 100%;
justify-content: flex-start;
}
.auth-container {
margin: 0 12px;
padding: 32px 24px;
}
.header-info {
position: static;
text-align: center;
margin-bottom: 12px;
}
.header h1 {
font-size: 1.5rem;
}
.header.compact h1 {
font-size: 1.25rem;
}
.auth-section {
padding: 48px 16px;
}
.card-header {
flex-direction: column;
align-items: flex-start;
}
.card-header div {
width: 100%;
justify-content: flex-start;
}
#loginPrompt > div {
flex-direction: column !important;
gap: 12px !important;
}
#loginPrompt > div > div:last-child {
width: 100%;
text-align: center;
}
#loginPrompt > div > div:first-child {
min-width: auto !important;
text-align: center;
}
}
/* === Utility Classes === */
.text-gradient {
background: var(--accent-gradient);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.glass {
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.3);
}
/* === Smooth Scrollbar === */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--bg-secondary);
}
::-webkit-scrollbar-thumb {
background: var(--border-color);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--text-muted);
}
/* === Selection === */
::selection {
background: rgba(99, 102, 241, 0.2);
color: var(--text-primary);
}
</style>
</head>
<body>
<div class="container">
<div class="header" id="mainHeader">
<div class="header-info">
<a href="https://github.com/dreamhunter2333/awsl-one-api" target="_blank">🔗 GitHub</a>
<span>v1.0.0</span>
</div>
<h1>🚀 Awsl One API</h1>
<p>管理面板 - 频道、令牌、定价管理</p>
<div id="logoutSection" style="display: none; position: absolute; top: 20px; right: 20px;">
<button class="btn btn-secondary btn-warning" onclick="confirmLogout()"
style="font-size: 12px; padding: 8px 16px;">
🚪 退出登录
</button>
</div>
</div>
<div id="authSection" class="auth-section" style="display: none;">
<div class="auth-container">
<div class="auth-title">管理员登录</div>
<form onsubmit="handleLogin(event)">
<input type="password" id="adminToken" class="auth-input" placeholder="请输入管理员令牌" />
<button type="submit" class="auth-btn">登录</button>
</form>
</div>
</div>
<!-- 登录模态框 -->
<div id="loginModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title">🔐 管理员登录</h3>
<button class="modal-close" onclick="closeLoginModal()">&times;</button>
</div>
<div class="modal-body">
<form onsubmit="handleModalLogin(event)">
<div class="form-group">
<label for="modalAdminToken">管理员令牌</label>
<input type="password" id="modalAdminToken" class="form-control" placeholder="请输入管理员令牌"
autocomplete="off" />
</div>
<button type="submit" class="btn" style="width: 100%; margin: 0;">
🔑 登录
</button>
</form>
</div>
</div>
</div>
<div id="mainContent">
<div class="tabs">
<button class="tab" onclick="switchTab('home')">🏠 首页</button>
<button class="tab admin-only" onclick="switchTab('system')" style="display: none;">⚙️ 系统管理</button>
<button class="tab admin-only" onclick="switchTab('channels')" style="display: none;">🔗 频道管理</button>
<button class="tab admin-only" onclick="switchTab('tokens')" style="display: none;">🔑 令牌管理</button>
<button class="tab admin-only" onclick="switchTab('pricing')" style="display: none;">💰 定价管理</button>
<button class="tab admin-only" onclick="switchTab('api-test')" style="display: none;">🧪 API 测试</button>
</div>
<!-- Home Tab -->
<div id="home" class="tab-content">
<!-- 未登录时的登录提示 -->
<div id="loginPrompt" class="card"
style="background: linear-gradient(135deg, rgba(245, 158, 11, 0.1) 0%, rgba(245, 158, 11, 0.05) 100%); border-left: 4px solid var(--warning-color); margin-bottom: 24px; padding: 18px 20px; border-color: rgba(245, 158, 11, 0.2);">
<div
style="display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap;">
<div style="flex: 1; min-width: 200px;">
<p style="margin: 0; color: #92400e; font-size: 14px; font-weight: 500; display: flex; align-items: center; gap: 8px;">
<span style="font-size: 18px;">💡</span> 要使用管理功能,请先进行身份验证
</p>
</div>
<div style="flex-shrink: 0;">
<button class="btn" onclick="showLoginModal()"
style="background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%); color: white; margin: 0; font-size: 13px; padding: 10px 18px;">
🔑 立即登录
</button>
</div>
</div>
</div>
<!-- 主要内容区域 -->
<div class="card">
<div class="card-header">
<h3 class="card-title" style="display: flex; align-items: center; gap: 10px;">
<span style="font-size: 24px;">🚀</span> 欢迎使用 AWSL One API
</h3>
</div>
<div style="margin-bottom: 32px;">
<p style="font-size: 16px; color: var(--text-secondary); line-height: 1.7; margin-bottom: 24px;">
AWSL One API 是一个强大的 API 代理和管理平台,为您提供统一的 AI 模型接口管理服务。
</p>
<div class="grid" style="margin-bottom: 32px;">
<div class="card feature-card"
style="margin-bottom: 0; background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); color: white; border: none;">
<h4 style="color: white; margin-bottom: 12px; font-size: 16px; font-weight: 600; display: flex; align-items: center; gap: 8px;">
<span style="font-size: 20px;">🔗</span> 频道管理
</h4>
<p style="margin: 0; opacity: 0.9; font-size: 14px; line-height: 1.6;">配置和管理 AI 服务提供商的 API 端点,支持 Azure OpenAI 等多种平台。</p>
</div>
<div class="card feature-card"
style="margin-bottom: 0; background: linear-gradient(135deg, #8b5cf6 0%, #a855f7 100%); color: white; border: none;">
<h4 style="color: white; margin-bottom: 12px; font-size: 16px; font-weight: 600; display: flex; align-items: center; gap: 8px;">
<span style="font-size: 20px;">🔑</span> 令牌管理
</h4>
<p style="margin: 0; opacity: 0.9; font-size: 14px; line-height: 1.6;">生成和管理 API 访问令牌,控制用户访问权限和使用配额。</p>
</div>
<div class="card feature-card"
style="margin-bottom: 0; background: linear-gradient(135deg, #10b981 0%, #059669 100%); color: white; border: none;">
<h4 style="color: white; margin-bottom: 12px; font-size: 16px; font-weight: 600; display: flex; align-items: center; gap: 8px;">
<span style="font-size: 20px;">💰</span> 定价管理
</h4>
<p style="margin: 0; opacity: 0.9; font-size: 14px; line-height: 1.6;">灵活配置不同 AI 模型的计费策略,精确控制使用成本。</p>
</div>
<div class="card feature-card"
style="margin-bottom: 0; background: linear-gradient(135deg, #f43f5e 0%, #e11d48 100%); color: white; border: none;">
<h4 style="color: white; margin-bottom: 12px; font-size: 16px; font-weight: 600; display: flex; align-items: center; gap: 8px;">
<span style="font-size: 20px;">🧪</span> API 测试
</h4>
<p style="margin: 0; opacity: 0.9; font-size: 14px; line-height: 1.6;">内置 API 测试工具,快速验证配置和调试接口问题。</p>
</div>
</div>
<div class="quick-start">
<h4>⚡ 快速开始</h4>
<ol>
<li>首先初始化数据库以设置必要的数据表结构</li>
<li>在频道管理中添加您的 AI 服务提供商配置</li>
<li>创建访问令牌并设置适当的权限和配额</li>
<li>配置模型定价策略以控制使用成本</li>
<li>使用 API 测试工具验证配置是否正常工作</li>
</ol>
</div>
</div>
</div>
</div>
<!-- System Management Tab -->
<div id="system" class="tab-content">
<div class="card">
<div class="card-header">
<h3 class="card-title">⚙️ 系统管理</h3>
</div>
<p style="color: #4a5568; margin-bottom: 20px;">管理系统数据库,包括初始化表结构等操作。如果这是首次使用,请先进行数据库初始化。</p>
<button class="btn" onclick="initializeDatabase()">🔄 初始化数据库</button>
</div>
</div>
<!-- Channels Tab -->
<div id="channels" class="tab-content">
<!-- Channel List View -->
<div id="channelListView">
<div class="card">
<div class="card-header">
<h3 class="card-title">频道列表</h3>
<div>
<button class="btn" onclick="showChannelForm()">➕ 添加频道</button>
<button class="btn btn-secondary" onclick="loadChannels()">🔄 刷新</button>
</div>
</div>
<div id="channelsList" class="item-list"></div>
</div>
</div>
<!-- Channel Form View -->
<div id="channelFormView" style="display: none;">
<div class="card">
<div class="card-header">
<h3 class="card-title" id="channelFormTitle">添加频道</h3>
<div>
<button class="btn btn-secondary" onclick="toggleChannelEditMode()"
id="channelModeToggle">📝
切换到JSON模式</button>
<button class="btn" onclick="saveChannel()">💾 保存频道</button>
<button class="btn btn-secondary" onclick="showChannelList()">← 返回列表</button>
</div>
</div>
<!-- 共用标识字段 -->
<div class="form-group">
<label for="channelKey">频道标识</label>
<input type="text" id="channelKey" class="form-control" placeholder="例如: azure-openai-1">
</div>
<!-- 表单模式 -->
<div id="channelFormMode">
<div class="form-group">
<label for="channelName">频道名称</label>
<input type="text" id="channelName" class="form-control"
placeholder="例如: My OpenAI Channel">
</div>
<div class="form-group">
<label for="channelType">频道类型</label>
<select id="channelType" class="form-control" onchange="toggleApiVersionField()">
<option value="azure-openai">Azure OpenAI</option>
<option value="openai">OpenAI</option>
</select>
</div>
<div class="form-group">
<label for="channelEndpoint">API 端点</label>
<input type="text" id="channelEndpoint" class="form-control"
placeholder="例如: https://xxx.openai.azure.com/">
</div>
<div class="form-group">
<label for="channelApiKey">API 密钥</label>
<input type="password" id="channelApiKey" class="form-control"
placeholder="your-api-key">
</div>
<div class="form-group" id="apiVersionGroup">
<label for="channelApiVersion">API 版本</label>
<input type="text" id="channelApiVersion" class="form-control"
placeholder="例如: 2024-02-01">
</div>
<div class="form-group">
<label for="deploymentMapper">模型部署映射 (JSON)</label>
<textarea id="deploymentMapper" class="form-control" rows="4"
placeholder='{"gpt-35-turbo": "gpt-35-turbo", "gpt-4": "gpt-4"}'></textarea>
<small style="color: #666; font-size: 12px;">配置模型名称映射关系(主要用于Azure OpenAI)</small>
</div>
<div class="form-group">
<label for="additionalConfig">其他配置 (JSON,可选)</label>
<textarea id="additionalConfig" class="form-control" rows="3"
placeholder='{"timeout": 30, "max_retries": 3}'></textarea>
<small style="color: #666; font-size: 12px;">其他自定义配置参数</small>
</div>
</div>
<!-- JSON模式 -->
<div id="channelJsonMode" style="display: none;">
<div class="form-group">
<label for="channelValueJson">频道配置 (JSON)</label>
<textarea id="channelValueJson" class="form-control" rows="15"
placeholder='{"name": "Azure OpenAI", "type": "azure-openai", "endpoint": "https://xxx.openai.azure.com/", "api_key": "your-api-key", "api_version": "2024-02-01", "deployment_mapper": {"gpt-35-turbo": "gpt-35-turbo", "gpt-4": "gpt-4"}}'></textarea>
<small style="color: #666; font-size: 12px;">直接编辑完整的频道配置JSON</small>
</div>
</div>
</div>
</div>
</div>
<!-- Tokens Tab -->
<div id="tokens" class="tab-content">
<!-- Token List View -->
<div id="tokenListView">
<div class="card">
<div class="card-header">
<h3 class="card-title">令牌列表</h3>
<div>
<button class="btn" onclick="showTokenForm()">➕ 添加令牌</button>
<button class="btn btn-secondary" onclick="loadTokens()">🔄 刷新</button>
</div>
</div>
<div id="tokensList" class="item-list"></div>
</div>
</div>
<!-- Token Form View -->
<div id="tokenFormView" style="display: none;">
<div class="card">
<div class="card-header">
<h3 class="card-title" id="tokenFormTitle">添加令牌</h3>
<div>
<button class="btn btn-secondary" onclick="toggleTokenEditMode()"
id="tokenModeToggle">📝
切换到JSON模式</button>
<button class="btn" onclick="saveToken()">💾 保存令牌</button>
<button class="btn btn-secondary" onclick="showTokenList()">← 返回列表</button>
</div>
</div>
<!-- 共用标识字段 -->
<div class="form-group">
<label for="tokenKey">令牌标识</label>
<div style="display: flex; gap: 10px; align-items: center;">
<input type="text" id="tokenKey" class="form-control" placeholder="输入令牌标识或点击生成">
<div id="tokenKeyActions">
<button type="button" id="generateTokenBtn" class="btn btn-secondary"
onclick="generateTokenKey()">🎲 生成</button>
<button type="button" id="copyTokenBtn" class="btn btn-secondary"
onclick="copyTokenKey()" style="display: none;">📋 复制</button>
</div>
</div>
</div>
<!-- 表单模式 -->
<div id="tokenFormMode">
<div class="form-group">
<label for="tokenName">令牌名称</label>
<input type="text" id="tokenName" class="form-control" placeholder="例如: 用户令牌1">
</div>
<div class="form-group">
<label for="channelKeysInput">允许访问的频道</label>
<input type="text" id="channelKeysInput" class="form-control"
placeholder="输入频道标识,多个用逗号分隔,留空表示允许所有频道">
<small style="color: #666; font-size: 12px;">例如: azure-openai-1, azure-openai-2
(留空表示允许访问所有频道)</small>
</div>
<div class="form-group">
<label for="totalQuotaInput">总配额</label>
<input type="number" id="totalQuotaInput" class="form-control" placeholder="1000000"
min="0" step="1">
<div style="margin-top: 10px;">
<small
style="color: #666; font-size: 12px; display: block; margin-bottom: 8px;">快速选择:</small>
<div style="display: flex; gap: 5px; flex-wrap: wrap;">
<button type="button" class="btn btn-secondary"
style="padding: 6px 12px; font-size: 12px; margin: 0;"
onclick="setQuotaValue(1000000)">$1</button>
<button type="button" class="btn btn-secondary"
style="padding: 6px 12px; font-size: 12px; margin: 0;"
onclick="setQuotaValue(5000000)">$5</button>
<button type="button" class="btn btn-secondary"
style="padding: 6px 12px; font-size: 12px; margin: 0;"
onclick="setQuotaValue(10000000)">$10</button>
<button type="button" class="btn btn-secondary"
style="padding: 6px 12px; font-size: 12px; margin: 0;"
onclick="setQuotaValue(20000000)">$20</button>
<button type="button" class="btn btn-secondary"
style="padding: 6px 12px; font-size: 12px; margin: 0;"
onclick="setQuotaValue(50000000)">$50</button>
</div>
</div>
<small style="color: #666; font-size: 12px; margin-top: 8px; display: block;">设置令牌的总使用配额
(基础单位:1百万token =
$1.00,例如:输入1000000表示1美元的配额)</small>
</div>
</div>
<!-- JSON模式 -->
<div id="tokenJsonMode" style="display: none;">
<div class="form-group">
<label for="tokenValueJson">令牌配置 (JSON)</label>
<textarea id="tokenValueJson" class="form-control" rows="12"
placeholder='{"name": "用户令牌1", "channel_keys": ["azure-openai-1", "azure-openai-2"], "total_quota": 1000000}'></textarea>
<small style="color: #666; font-size: 12px;">直接编辑完整的令牌配置JSON</small>
</div>
</div>
</div>
</div>
</div>
<!-- Pricing Tab -->
<div id="pricing" class="tab-content">
<div class="card">
<div class="card-header">
<h3 class="card-title">定价配置</h3>
<div>
<button class="btn btn-secondary" onclick="togglePricingEditMode()"
id="pricingModeToggle">📝
切换到JSON模式</button>
<button class="btn btn-secondary" onclick="addPricingModel()" id="addPricingModelBtn">➕
添加模型</button>
<button class="btn" onclick="updatePricing()">💾 更新定价</button>
<button class="btn btn-secondary" onclick="loadPricing()">🔄 刷新</button>
</div>
</div>
<div
style="background: #f8f9fa; padding: 15px; border-radius: 6px; margin-bottom: 20px; border-left: 4px solid #4c51bf;">
<h4 style="margin: 0 0 10px 0; color: #2d3748; font-size: 14px;">💡 定价说明</h4>
<p style="margin: 0; font-size: 13px; color: #4a5568; line-height: 1.5;">
<strong>基础配额单位:1百万token = $1.00</strong> | 定价配置为不同模型相对于基础单位的倍率 |
例如:gpt-4的input倍率为30,表示1百万token消耗30美元配额
</p>
</div>
<div id="pricingContent">
<!-- 表格模式 -->
<div id="pricingTableMode" class="pricing-table">
<table style="width: 100%; border-collapse: collapse; margin-top: 10px;">
<thead>
<tr style="background-color: #f8f9fa; border-bottom: 2px solid #e9ecef;">
<th
style="padding: 12px; text-align: left; font-weight: 600; color: #2d3748; border: 1px solid #e9ecef;">
模型名称</th>
<th
style="padding: 12px; text-align: left; font-weight: 600; color: #2d3748; border: 1px solid #e9ecef;">
输入倍率</th>
<th
style="padding: 12px; text-align: left; font-weight: 600; color: #2d3748; border: 1px solid #e9ecef;">
输出倍率</th>
<th
style="padding: 12px; text-align: center; font-weight: 600; color: #2d3748; border: 1px solid #e9ecef; width: 100px;">
操作</th>
</tr>
</thead>
<tbody id="pricingTableBody">
<!-- 动态生成的定价行 -->
</tbody>
</table>
</div>
<!-- JSON模式 -->
<div id="pricingJsonMode" style="display: none;">
<div class="form-group">
<label for="pricingConfigJson">定价配置 (JSON)</label>
<textarea id="pricingConfigJson" class="form-control" rows="15"
placeholder='{"gpt-3.5-turbo": {"input": 0.001, "output": 0.002}, "gpt-4": {"input": 0.03, "output": 0.06}}'></textarea>
<small style="color: #666; font-size: 12px; margin-top: 8px; display: block;">
配置格式:模型名称 -> {input: 输入倍率, output: 输出倍率}。倍率基于1百万token=$1.00计算
</small>
</div>
</div>
</div>
</div>
</div>
<!-- API Test Tab -->
<div id="api-test" class="tab-content">
<div class="card">
<div class="card-header">
<h3 class="card-title">API 测试</h3>
<div>
<button class="btn" onclick="sendApiTest()">🚀 发送请求</button>
<button class="btn btn-secondary" onclick="clearApiTest()">🧹 清空</button>
</div>
</div>
<div class="grid">
<div>
<div class="form-group">
<label for="apiTestToken">Bearer Token</label>
<input type="text" id="apiTestToken" class="form-control"
placeholder="输入你的 API Token (sk-...)">
<small style="color: #666; font-size: 12px;">从令牌管理页面复制你的 API Token</small>
</div>
<div class="form-group">
<label for="apiTestRequest">请求 JSON</label>
<textarea id="apiTestRequest" class="form-control" rows="12" placeholder='输入请求 JSON...'>{
"model": "gpt-4",
"messages": [
{
"role": "user",
"content": "Hello, world!"
}
]
}</textarea>
<small style="color: #666; font-size: 12px;">请求将发送到 /v1/chat/completions 端点</small>
</div>
</div>
<div>
<div class="form-group">
<label for="apiTestResponse">响应 JSON</label>
<textarea id="apiTestResponse" class="form-control" rows="16"
placeholder="API 响应将显示在这里..." readonly></textarea>
<div id="apiTestStatus"
style="margin-top: 10px; padding: 10px; border-radius: 6px; display: none;"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
// === 全局变量 ===
let adminToken = '';
let currentEditingChannel = null;
let currentEditingToken = null;
// === 通用工具函数 ===
// DOM元素操作工具类
const DOMUtils = {
// 获取元素并检查是否存在
getElement(id) {
const element = document.getElementById(id);
if (!element) {
console.warn(`Element with id '${id}' not found`);
}
return element;
},
// 显示/隐藏元素
show(elementOrId) {
const element = typeof elementOrId === 'string' ? this.getElement(elementOrId) : elementOrId;
if (element) element.style.display = 'block';
},
hide(elementOrId) {
const element = typeof elementOrId === 'string' ? this.getElement(elementOrId) : elementOrId;
if (element) element.style.display = 'none';
},
// 切换class
toggleClass(elementOrId, className, force) {
const element = typeof elementOrId === 'string' ? this.getElement(elementOrId) : elementOrId;
if (element) {
if (force !== undefined) {
element.classList.toggle(className, force);
} else {
element.classList.toggle(className);
}
}
},
// 清空输入框
clearInput(id) {
const element = this.getElement(id);
if (element) element.value = '';
},
// 设置输入框值
setValue(id, value) {
const element = this.getElement(id);
if (element) element.value = value || '';
},
// 获取输入框值
getValue(id) {
const element = this.getElement(id);
return element ? element.value.trim() : '';
},
// 聚焦元素
focus(id, delay = 0) {
const element = this.getElement(id);
if (element) {
if (delay > 0) {
setTimeout(() => element.focus(), delay);
} else {
element.focus();
}
}
}
};
// 表单状态管理工具类
const FormUtils = {
// 设置输入框为只读状态
setReadonly(id, readonly = true) {
const element = DOMUtils.getElement(id);
if (element) {
element.readOnly = readonly;
element.disabled = readonly;
element.style.cursor = readonly ? 'not-allowed' : 'text';
element.style.backgroundColor = readonly ? '#f8f9fa' : '';
element.placeholder = readonly ? '编辑时不可修改' : element.getAttribute('data-original-placeholder') || '';
}
},
// 保存原始placeholder
saveOriginalPlaceholder(id) {
const element = DOMUtils.getElement(id);
if (element && !element.getAttribute('data-original-placeholder')) {
element.setAttribute('data-original-placeholder', element.placeholder);
}
},
// 验证必填字段
validateRequired(fields) {
for (const field of fields) {
const value = DOMUtils.getValue(field.id);
if (!value) {
showAlert(field.message || `请填写${field.name}`, 'error');
return false;
}
}
return true;
},
// 验证JSON格式
validateJSON(jsonString, errorMessage = 'JSON格式错误') {
try {
return JSON.parse(jsonString);
} catch (error) {
showAlert(errorMessage, 'error');
return null;
}
}
};
// Tab管理工具类
const TabUtils = {
// 激活tab
activateTab(tabName) {
// 隐藏所有tab内容
document.querySelectorAll('.tab-content').forEach(tab => tab.classList.remove('active'));
document.querySelectorAll('.tab').forEach(btn => btn.classList.remove('active'));
// 显示目标tab
const targetTab = DOMUtils.getElement(tabName);
if (targetTab) targetTab.classList.add('active');
// 激活对应按钮
const targetButton = document.querySelector(`.tab[onclick="switchTab('${tabName}')"]`);
if (targetButton) targetButton.classList.add('active');
},
// 设置管理员tabs可见性
setAdminTabsVisible(visible = true) {
document.querySelectorAll('.admin-only').forEach(tab => {
tab.style.display = visible ? 'block' : 'none';
});
}
};
// 认证状态管理
const AuthManager = {
// 检查是否已登录
isAuthenticated() {
return !!adminToken;
},
// 设置token
setToken(token) {
adminToken = token;
localStorage.setItem('adminToken', token);
},
// 清除token
clearToken() {
adminToken = '';
localStorage.removeItem('adminToken');
},
// 从localStorage恢复token
restoreToken() {
const savedToken = localStorage.getItem('adminToken');
if (savedToken) {
adminToken = savedToken;
return true;
}
return false;
}
};
// === 认证相关函数 ===
// 显示登录模态框
function showLoginModal() {
DOMUtils.show('loginModal');
DOMUtils.clearInput('modalAdminToken');
DOMUtils.focus('modalAdminToken', 100);
}
// 关闭登录模态框
function closeLoginModal() {
DOMUtils.hide('loginModal');
}
// 处理模态框登录表单提交
function handleModalLogin(event) {
event.preventDefault();
authenticateFromModal();
}
// 模态框身份验证
function authenticateFromModal() {
const token = DOMUtils.getValue('modalAdminToken');
if (!token) {
showAlert('请输入管理员令牌', 'error');
return;
}
AuthManager.setToken(token);
closeLoginModal();
showMainContent();
showAlert('登录成功!', 'success');
}
// 显示登录页面 (保留兼容性)
function showLoginPage() {
showLoginModal();
}
// 显示主页面(未登录状态)
function showHomePage() {
DOMUtils.hide('authSection');
DOMUtils.show('mainContent');
DOMUtils.hide('logoutSection');
DOMUtils.toggleClass('mainHeader', 'compact', true);
// 显示登录提示
DOMUtils.show('loginPrompt');
// 隐藏管理员专用的tabs
TabUtils.setAdminTabsVisible(false);
// 激活首页tab
TabUtils.activateTab('home');
}
// 处理登录表单提交(支持回车键)
function handleLogin(event) {
event.preventDefault();
authenticate();
}
// Authentication
function authenticate() {
const token = DOMUtils.getValue('adminToken');
if (!token) {
showAlert('请输入管理员令牌', 'error');
return;
}
AuthManager.setToken(token);
showMainContent();
}
// 显示主内容区域(已登录状态)
function showMainContent() {
DOMUtils.hide('authSection');
DOMUtils.show('mainContent');
DOMUtils.show('logoutSection');
DOMUtils.toggleClass('mainHeader', 'compact', true);
// 隐藏登录提示
DOMUtils.hide('loginPrompt');
// 显示管理员专用的tabs
TabUtils.setAdminTabsVisible(true);
// 激活首页tab
TabUtils.activateTab('home');
}
// 确认退出登录
function confirmLogout() {
if (confirm('确定要退出登录吗?\n\n退出后您需要重新输入管理员令牌才能使用管理功能。')) {
logout();
}
}
// 退出登录
function logout() {
AuthManager.clearToken();
showHomePage();
DOMUtils.clearInput('adminToken');
showAlert('已退出登录', 'success');
}
// Tab switching
function switchTab(tabName) {
// 检查是否为管理员专用tab且用户未登录
const adminOnlyTabs = ['system', 'channels', 'tokens', 'pricing', 'api-test'];
if (adminOnlyTabs.includes(tabName) && !AuthManager.isAuthenticated()) {
showAlert('请先登录后再访问管理功能', 'error');
return;
}
// 隐藏所有表单视图,显示列表视图
if (tabName === 'channels') {
showChannelList();
} else if (tabName === 'tokens') {
showTokenList();
}
// 激活tab
TabUtils.activateTab(tabName);
// 切换tab时重新获取数据
const tabDataLoaders = {
'channels': loadChannels,
'tokens': loadTokens,
'pricing': loadPricing
};
const loader = tabDataLoaders[tabName];
if (loader) {
loader();
}
}
// === API请求工具函数 ===
// API request helper
async function apiRequest(url, method = 'GET', body = null, showGlobalLoading = true, loadingMessage = '正在处理请求...') {
const options = {
method,
headers: {
'Content-Type': 'application/json',
'x-admin-token': adminToken
}
};
if (body) {
options.body = JSON.stringify(body);
}
// 显示全局 loading
if (showGlobalLoading) {
showLoading(loadingMessage);
}
try {
const response = await fetch(url, options);
if (!response.ok) {
const errorText = await response.text();
throw new Error(`HTTP ${response.status}: ${errorText}`);
}
const result = await response.json();
// 处理 CommonResponse 格式
if (result.success === false) {
throw new Error(result.message || '请求失败');
}
// 返回实际数据,如果有 data 字段则返回 data,否则返回整个结果
return result.data !== undefined ? result.data : result;
} catch (error) {
console.error('API Error:', error);
throw error;
} finally {
// 无论成功还是失败都隐藏 loading
if (showGlobalLoading) {
hideLoading();
}
}
}
// Database functions
async function initializeDatabase() {
try {
await apiRequest('/api/admin/db_initialize', 'POST', null, true, '正在初始化数据库...');
showAlert('数据库初始化成功!', 'success');
} catch (error) {
showAlert(`数据库初始化失败: ${error.message}`, 'error');
}
}
// === 表单编辑模式管理 ===
// 编辑模式管理类
class EditModeManager {
constructor(formModeId, jsonModeId, toggleBtnId, formModeText, jsonModeText) {
this.formModeElement = DOMUtils.getElement(formModeId);
this.jsonModeElement = DOMUtils.getElement(jsonModeId);
this.toggleBtn = DOMUtils.getElement(toggleBtnId);
this.formModeText = formModeText;
this.jsonModeText = jsonModeText;
this.currentMode = 'form';
}
// 切换编辑模式
toggle(formToJsonConverter, jsonToFormConverter) {
if (this.currentMode === 'form') {
this._switchToJsonMode(formToJsonConverter);
} else {
this._switchToFormMode(jsonToFormConverter);
}
}
_switchToJsonMode(converter) {
if (converter) {
const data = converter();
if (data && this.jsonModeElement) {
const textarea = this.jsonModeElement.querySelector('textarea');
if (textarea) {
textarea.value = JSON.stringify(data, null, 2);
}
}
}
DOMUtils.hide(this.formModeElement);
DOMUtils.show(this.jsonModeElement);
if (this.toggleBtn) this.toggleBtn.textContent = this.formModeText;
this.currentMode = 'json';
}
_switchToFormMode(converter) {
if (converter) {
try {
const textarea = this.jsonModeElement?.querySelector('textarea');
const jsonValue = textarea?.value;
if (jsonValue) {
const parsed = JSON.parse(jsonValue);
converter(parsed);
}
} catch (error) {
console.warn('JSON格式错误,无法转换到表单模式:', error);
}
}
DOMUtils.show(this.formModeElement);
DOMUtils.hide(this.jsonModeElement);
if (this.toggleBtn) this.toggleBtn.textContent = this.jsonModeText;
this.currentMode = 'form';
}
// 重置到表单模式
resetToFormMode() {
this.currentMode = 'form';
DOMUtils.show(this.formModeElement);
DOMUtils.hide(this.jsonModeElement);
if (this.toggleBtn) this.toggleBtn.textContent = this.jsonModeText;
}
// 获取当前模式
getCurrentMode() {
return this.currentMode;
}
}
// === CHANNEL FUNCTIONS ===
// 全局变量跟踪编辑模式
let channelEditMode = 'form';
let tokenEditMode = 'form';
let pricingEditMode = 'table';
// 创建频道编辑模式管理器
const channelModeManager = new EditModeManager(
'channelFormMode',
'channelJsonMode',
'channelModeToggle',
'📋 切换到表单模式',
'📝 切换到JSON模式'
);
// 切换频道编辑模式
function toggleChannelEditMode() {
channelModeManager.toggle(
collectChannelFormData,
(parsed) => {
const key = DOMUtils.getValue('channelKey');
fillChannelForm(key, parsed);
}
);
channelEditMode = channelModeManager.getCurrentMode();
}
// 收集频道表单数据
function collectChannelFormData() {
const requiredFields = [
{ id: 'channelName', name: '频道名称' },
{ id: 'channelEndpoint', name: 'API端点' },
{ id: 'channelApiKey', name: 'API密钥' }
];
// 检查必填字段
for (const field of requiredFields) {
if (!DOMUtils.getValue(field.id)) {
return null;
}
}
const config = {
name: DOMUtils.getValue('channelName'),
type: DOMUtils.getValue('channelType'),
endpoint: DOMUtils.getValue('channelEndpoint'),
api_key: DOMUtils.getValue('channelApiKey')
};
// 可选字段
const apiVersion = DOMUtils.getValue('channelApiVersion');
if (apiVersion) {
config.api_version = apiVersion;
}
// 处理JSON字段
const deploymentMapperText = DOMUtils.getValue('deploymentMapper');
if (deploymentMapperText) {
const parsed = FormUtils.validateJSON(deploymentMapperText, '部署映射JSON格式错误');
if (parsed) config.deployment_mapper = parsed;
}
const additionalConfigText = DOMUtils.getValue('additionalConfig');
if (additionalConfigText) {
const parsed = FormUtils.validateJSON(additionalConfigText, '其他配置JSON格式错误');
if (parsed) Object.assign(config, parsed);
}
return config;
}
// 填充频道表单
function fillChannelForm(key, config) {
const fieldMappings = [
{ id: 'channelKey', value: key },
{ id: 'channelName', value: config.name },
{ id: 'channelType', value: config.type },
{ id: 'channelEndpoint', value: config.endpoint },
{ id: 'channelApiKey', value: config.api_key },
{ id: 'channelApiVersion', value: config.api_version }
];
fieldMappings.forEach(field => DOMUtils.setValue(field.id, field.value));
// 处理特殊字段
if (config.deployment_mapper) {
DOMUtils.setValue('deploymentMapper', JSON.stringify(config.deployment_mapper, null, 2));
} else {
DOMUtils.clearInput('deploymentMapper');
}
// 处理其他配置
const additionalConfig = { ...config };
['name', 'type', 'endpoint', 'api_key', 'api_version', 'deployment_mapper'].forEach(key => {
delete additionalConfig[key];
});
if (Object.keys(additionalConfig).length > 0) {
DOMUtils.setValue('additionalConfig', JSON.stringify(additionalConfig, null, 2));
} else {
DOMUtils.clearInput('additionalConfig');
}
}
// 切换API版本字段的显示状态
function toggleApiVersionField() {
const channelType = DOMUtils.getValue('channelType');
const apiVersionGroup = DOMUtils.getElement('apiVersionGroup');
if (channelType === 'azure-openai') {
DOMUtils.show(apiVersionGroup);
} else {
DOMUtils.hide(apiVersionGroup);
DOMUtils.clearInput('channelApiVersion');
}
}
// 显示频道列表
function showChannelList() {
DOMUtils.show('channelListView');
DOMUtils.hide('channelFormView');
currentEditingChannel = null;
}
// 显示频道表单
function showChannelForm(isEdit = false) {
DOMUtils.hide('channelListView');
DOMUtils.show('channelFormView');
DOMUtils.setValue('channelFormTitle', isEdit ? '编辑频道' : '添加频道');
if (!isEdit) {
clearChannelForm();
}
}
// 清空频道表单
function clearChannelForm() {
// 清空所有输入字段
const fieldIds = [
'channelKey', 'channelName', 'channelEndpoint',
'channelApiKey', 'channelApiVersion', 'deploymentMapper',
'additionalConfig', 'channelValueJson'
];
fieldIds.forEach(id => DOMUtils.clearInput(id));
// 重置表单状态
FormUtils.setReadonly('channelKey', false);
DOMUtils.setValue('channelType', 'openai');
DOMUtils.getElement('channelKey').placeholder = '例如: openai-1';
// 重置编辑模式
channelModeManager.resetToFormMode();
channelEditMode = 'form';
currentEditingChannel = null;
// 确保API版本字段根据类型正确显示
toggleApiVersionField();
}
// 编辑频道
function editChannelFromList(key, originalValue) {
currentEditingChannel = key;
const parsed = FormUtils.validateJSON(originalValue, '频道配置格式错误');
if (!parsed) return;
// 设置key字段为只读
FormUtils.saveOriginalPlaceholder('channelKey');
FormUtils.setReadonly('channelKey', true);
DOMUtils.setValue('channelKey', key);
if (channelEditMode === 'form') {
fillChannelForm(key, parsed);
} else {
DOMUtils.setValue('channelValueJson', JSON.stringify(parsed, null, 2));
}
toggleApiVersionField();
showChannelForm(true);
}
// 保存频道
async function saveChannel() {
const key = DOMUtils.getValue('channelKey');
if (!key) {
showAlert('请填写频道标识', 'error');
return;
}
let config;
if (channelEditMode === 'form') {
config = collectChannelFormData();
if (!config) {
showAlert('请填写所有必填字段(频道名称、端点、API密钥)', 'error');
return;
}
} else {
const jsonValue = DOMUtils.getValue('channelValueJson');
if (!jsonValue) {
showAlert('请填写配置JSON', 'error');
return;
}
config = FormUtils.validateJSON(jsonValue, 'JSON格式错误,请检查配置格式');
if (!config) return;
}
try {
await apiRequest(`/api/admin/channel/${encodeURIComponent(key)}`, 'POST', config, true, '正在保存频道...');
showAlert('频道保存成功!', 'success');
clearChannelForm();
showChannelList();
loadChannels();
} catch (error) {
showAlert(`保存频道失败: ${error.message}`, 'error');
}
}
// Channel functions
async function loadChannels() {
try {
const channels = await apiRequest('/api/admin/channel', 'GET', null, true);
displayChannels(channels);
} catch (error) {
DOMUtils.getElement('channelsList').innerHTML = `<div class="alert alert-error">加载频道失败: ${error.message}</div>`;
}
}
async function deleteChannel(key) {
if (!confirm('确定要删除这个频道吗?')) return;
try {
await apiRequest(`/api/admin/channel/${encodeURIComponent(key)}`, 'DELETE', null, true, '正在删除频道...');
showAlert('频道删除成功!', 'success');
loadChannels();
} catch (error) {
showAlert(`删除频道失败: ${error.message}`, 'error');
}
}
// 显示频道列表的通用函数
function displayChannels(channels) {
const container = DOMUtils.getElement('channelsList');
if (!container) return;
// 处理空数据的情况
if (!channels || !Array.isArray(channels) || channels.length === 0) {
container.innerHTML = '<div class="alert alert-error">暂无频道数据</div>';
return;
}
container.innerHTML = channels.map(item => {
let parsedValue = {};
try {
parsedValue = JSON.parse(item.value);
} catch {
parsedValue = { name: 'Invalid JSON', type: 'azure-openai', endpoint: '' };
}
const name = parsedValue.name || 'No name';
const type = parsedValue.type || 'Unknown';
const endpoint = (parsedValue.endpoint || '');
const endpointDisplay = endpoint.length > 50 ? endpoint.substring(0, 50) + '...' : endpoint;
return `
<div class="item">
<div class="item-info">
<div class="item-key">${name}</div>
<div class="item-details">
${item.key || 'Unknown'} | 类型: ${type} | 端点: ${endpointDisplay}
</div>
</div>
<div class="actions">
<button class="btn btn-secondary" onclick="editChannelFromList('${item.key}', '${item.value.replace(/'/g, '\\\'').replace(/"/g, '&quot;')}')">✏️ 编辑</button>
<button class="btn btn-danger" onclick="deleteChannel('${item.key}')">🗑️ 删除</button>
</div>
</div>
`;
}).join('');
}
// === TOKEN FUNCTIONS ===
// 创建令牌编辑模式管理器
const tokenModeManager = new EditModeManager(
'tokenFormMode',
'tokenJsonMode',
'tokenModeToggle',
'📋 切换到表单模式',
'📝 切换到JSON模式'
);
// 切换令牌编辑模式
function toggleTokenEditMode() {
tokenModeManager.toggle(
() => {
const formData = collectTokenFormData();
delete formData.key; // 从配置中移除key,因为key现在是共用的
return formData;
},
(parsed) => {
const key = DOMUtils.getValue('tokenKey');
fillTokenForm(key, parsed);
}
);
tokenEditMode = tokenModeManager.getCurrentMode();
}
// 收集令牌表单数据
function collectTokenFormData() {
const channelKeysInput = DOMUtils.getValue('channelKeysInput');
const channelKeys = channelKeysInput
? channelKeysInput.split(',').map(key => key.trim()).filter(key => key.length > 0)
: [];
return {
key: DOMUtils.getValue('tokenKey'),
name: DOMUtils.getValue('tokenName'),
channel_keys: channelKeys,
total_quota: parseFloat(DOMUtils.getValue('totalQuotaInput')) || 0
};
}
// 填充令牌表单
function fillTokenForm(key, config) {
DOMUtils.setValue('tokenName', config.name);
DOMUtils.setValue('tokenKey', key);
const channelKeysStr = config.channel_keys && Array.isArray(config.channel_keys)
? config.channel_keys.join(', ')
: '';
DOMUtils.setValue('channelKeysInput', channelKeysStr);
DOMUtils.setValue('totalQuotaInput', config.total_quota);
}
// Token按钮状态管理类
class TokenButtonManager {
static setEditMode(isEdit) {
const tokenKeyInput = DOMUtils.getElement('tokenKey');
const generateBtn = DOMUtils.getElement('generateTokenBtn');
const copyBtn = DOMUtils.getElement('copyTokenBtn');
if (isEdit) {
// 编辑模式:只显示复制按钮,token key 不可编辑
FormUtils.setReadonly('tokenKey', true);
DOMUtils.hide(generateBtn);
DOMUtils.show(copyBtn);
} else {
// 创建模式:显示生成按钮,token key 可编辑
FormUtils.setReadonly('tokenKey', false);
DOMUtils.show(generateBtn);
DOMUtils.hide(copyBtn);
if (tokenKeyInput) {
tokenKeyInput.placeholder = '输入令牌标识或点击生成';
}
}
}
}
// 显示令牌列表
function showTokenList() {
DOMUtils.show('tokenListView');
DOMUtils.hide('tokenFormView');
currentEditingToken = null;
}
// 显示令牌表单
function showTokenForm(isEdit = false) {
DOMUtils.hide('tokenListView');
DOMUtils.show('tokenFormView');
const titleElement = DOMUtils.getElement('tokenFormTitle');
if (titleElement) titleElement.textContent = isEdit ? '编辑令牌' : '添加令牌';
TokenButtonManager.setEditMode(isEdit);
if (!isEdit) {
clearTokenForm();
generateTokenKey(); // 自动生成token标识
}
}
// 清空令牌表单
function clearTokenForm() {
// 清空所有字段
const fieldIds = ['tokenName', 'channelKeysInput', 'totalQuotaInput', 'tokenValueJson'];
fieldIds.forEach(id => DOMUtils.clearInput(id));
// 重置token key输入框状态
FormUtils.saveOriginalPlaceholder('tokenKey');
FormUtils.setReadonly('tokenKey', false);
DOMUtils.clearInput('tokenKey');
const tokenKeyInput = DOMUtils.getElement('tokenKey');
if (tokenKeyInput) {
tokenKeyInput.placeholder = '输入令牌标识或点击生成';
}
// 重置编辑模式
tokenModeManager.resetToFormMode();
tokenEditMode = 'form';
currentEditingToken = null;
}
// 生成token key
function generateTokenKey() {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = 'sk-';
for (let i = 0; i < 48; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
DOMUtils.setValue('tokenKey', result);
}
// 复制token key
function copyTokenKey() {
const tokenKey = DOMUtils.getValue('tokenKey');
if (!tokenKey) {
showAlert('请先生成令牌标识', 'error');
return;
}
copyToClipboard(tokenKey);
}
// 设置配额值
function setQuotaValue(value) {
DOMUtils.setValue('totalQuotaInput', value);
}
// 从列表编辑令牌
function editTokenFromList(key, originalValue) {
currentEditingToken = key;
const parsed = FormUtils.validateJSON(originalValue, '令牌配置格式错误');
if (!parsed) return;
// 设置key字段状态
FormUtils.saveOriginalPlaceholder('tokenKey');
FormUtils.setReadonly('tokenKey', true);
DOMUtils.setValue('tokenKey', key);
// 根据模式填充数据
if (tokenEditMode === 'form') {
fillTokenForm(key, parsed);
} else {
DOMUtils.setValue('tokenValueJson', JSON.stringify(parsed, null, 2));
}
TokenButtonManager.setEditMode(true);
showTokenForm(true);
}
// 保存令牌
async function saveToken() {
const key = DOMUtils.getValue('tokenKey');
if (!key) {
showAlert('请填写令牌标识', 'error');
return;
}
let config;
if (tokenEditMode === 'form') {
const formData = collectTokenFormData();
config = {
name: formData.name,
channel_keys: formData.channel_keys,
total_quota: formData.total_quota
};
// 验证必填字段
if (!FormUtils.validateRequired([
{ id: 'tokenName', name: '令牌名称', message: '请填写令牌名称' }
])) {
return;
}
if (!formData.total_quota || isNaN(formData.total_quota) || formData.total_quota < 0) {
showAlert('请输入有效的总配额数值', 'error');
return;
}
} else {
const jsonValue = DOMUtils.getValue('tokenValueJson');
if (!jsonValue) {
showAlert('请填写配置JSON', 'error');
return;
}
config = FormUtils.validateJSON(jsonValue, 'JSON格式错误,请检查配置格式');
if (!config) return;
}
try {
await apiRequest(`/api/admin/token/${encodeURIComponent(key)}`, 'POST', config, true, '正在保存令牌...');
showAlert('令牌保存成功!', 'success');
clearTokenForm();
showTokenList();
loadTokens();
} catch (error) {
showAlert(`保存令牌失败: ${error.message}`, 'error');
}
}
// Token functions
async function loadTokens() {
try {
const tokens = await apiRequest('/api/admin/token', 'GET', null, true);
displayTokens(tokens);
} catch (error) {
DOMUtils.getElement('tokensList').innerHTML = `<div class="alert alert-error">加载令牌失败: ${error.message}</div>`;
}
}
async function deleteToken(key) {
if (!confirm('确定要删除这个令牌吗?')) return;
try {
await apiRequest(`/api/admin/token/${encodeURIComponent(key)}`, 'DELETE', null, true, '正在删除令牌...');
showAlert('令牌删除成功!', 'success');
loadTokens();
} catch (error) {
showAlert(`删除令牌失败: ${error.message}`, 'error');
}
}
// 显示令牌列表的通用函数
function displayTokens(tokens) {
const container = DOMUtils.getElement('tokensList');
if (!container) return;
// 处理空数据的情况
if (!tokens || !Array.isArray(tokens) || tokens.length === 0) {
container.innerHTML = '<div class="alert alert-error">暂无令牌数据</div>';
return;
}
container.innerHTML = tokens.map(item => {
let parsedValue = {};
try {
parsedValue = JSON.parse(item.value);
} catch {
parsedValue = { name: 'Invalid JSON', channel_keys: ['Invalid JSON'], total_quota: 0 };
}
// 处理 channel_keys 显示
const channelsDisplay = parsedValue.channel_keys && parsedValue.channel_keys.length > 0
? parsedValue.channel_keys.join(', ')
: '所有频道';
// 将配额转换为美元显示
const totalQuotaValue = parsedValue.total_quota || 0;
const quotaInDollars = (totalQuotaValue / 1000000).toFixed(2);
const usageValue = item.usage || 0;
const usageInDollars = (usageValue / 1000000).toFixed(2);
const tokenName = parsedValue.name || '未命名';
return `
<div class="item">
<div class="item-info">
<div class="item-key">${tokenName}</div>
<div class="item-details">
频道: ${channelsDisplay} | 配额: $${quotaInDollars} | 已用: $${usageInDollars}
</div>
</div>
<div class="actions">
<button class="btn btn-secondary" onclick="copyToClipboard('${item.key}')" title="复制token">📋 复制</button>
<button class="btn btn-secondary" onclick="editTokenFromList('${item.key}', '${item.value.replace(/'/g, '\\\'').replace(/"/g, '&quot;')}')">✏️ 编辑</button>
<button class="btn btn-danger" onclick="deleteToken('${item.key}')">🗑️ 删除</button>
</div>
</div>
`;
}).join('');
}
// === PRICING FUNCTIONS ===
// 创建定价编辑模式管理器
const pricingModeManager = new EditModeManager(
'pricingTableMode',
'pricingJsonMode',
'pricingModeToggle',
'📊 切换到表格模式',
'📝 切换到JSON模式'
);
// 切换定价编辑模式
function togglePricingEditMode() {
const addBtn = DOMUtils.getElement('addPricingModelBtn');
pricingModeManager.toggle(
collectPricingTableData,
(parsed) => displayPricingTable(parsed)
);
pricingEditMode = pricingModeManager.getCurrentMode();
// 控制添加模型按钮的显示
if (addBtn) {
if (pricingEditMode === 'table') {
DOMUtils.show(addBtn);
} else {
DOMUtils.hide(addBtn);
}
}
}
// 收集定价表格数据
function collectPricingTableData() {
const tbody = DOMUtils.getElement('pricingTableBody');
const config = {};
if (!tbody) return config;
Array.from(tbody.children).forEach((row) => {
const inputs = row.querySelectorAll('input');
if (inputs.length < 3) return;
const modelName = inputs[0].value.trim();
const inputPrice = parseFloat(inputs[1].value) || 0;
const outputPrice = parseFloat(inputs[2].value) || 0;
if (modelName) {
config[modelName] = {
input: inputPrice,
output: outputPrice
};
}
});
return config;
}
// 添加新的定价模型行
function addPricingModel() {
const tbody = DOMUtils.getElement('pricingTableBody');
if (!tbody) return;
const rowIndex = tbody.children.length;
const row = document.createElement('tr');
row.style.borderBottom = '1px solid #e9ecef';
row.innerHTML = `
<td style="padding: 12px; border: 1px solid #e9ecef;">
<input type="text" class="form-control" placeholder="例如: gpt-4"
onchange="validatePricingRow(${rowIndex})" style="border: none; padding: 8px; background: transparent;">
</td>
<td style="padding: 12px; border: 1px solid #e9ecef;">
<input type="number" class="form-control" placeholder="0.03" step="0.001" min="0"
onchange="validatePricingRow(${rowIndex})" style="border: none; padding: 8px; background: transparent;">
</td>
<td style="padding: 12px; border: 1px solid #e9ecef;">
<input type="number" class="form-control" placeholder="0.06" step="0.001" min="0"
onchange="validatePricingRow(${rowIndex})" style="border: none; padding: 8px; background: transparent;">
</td>
<td style="padding: 12px; border: 1px solid #e9ecef; text-align: center;">
<button class="btn btn-danger" onclick="removePricingRow(this)" style="font-size: 12px; padding: 6px 12px; white-space: nowrap;">🗑️ 删除</button>
</td>
`;
tbody.appendChild(row);
}
// 删除定价模型行
function removePricingRow(button) {
const row = button.closest('tr');
if (row) row.remove();
}
// 验证定价行数据
function validatePricingRow(rowIndex) {
// 这里可以添加验证逻辑,比如检查模型名称是否重复等
}
// Pricing functions
async function loadPricing() {
try {
const pricing = await apiRequest('/api/admin/pricing', 'GET', null, true);
const addBtn = DOMUtils.getElement('addPricingModelBtn');
if (pricingEditMode === 'table') {
displayPricingTable(pricing);
if (addBtn) DOMUtils.show(addBtn);
} else {
DOMUtils.setValue('pricingConfigJson', JSON.stringify(pricing, null, 2));
if (addBtn) DOMUtils.hide(addBtn);
}
} catch (error) {
// 失败时显示错误信息并重新创建表格结构
if (pricingEditMode === 'table') {
const pricingContent = DOMUtils.getElement('pricingContent');
if (pricingContent) {
pricingContent.innerHTML = `
<div class="alert alert-error">加载定价配置失败: ${error.message}</div>
<div id="pricingTableMode" class="pricing-table">
<table style="width: 100%; border-collapse: collapse; margin-top: 10px;">
<thead>
<tr style="background-color: #f8f9fa; border-bottom: 2px solid #e9ecef;">
<th style="padding: 12px; text-align: left; font-weight: 600; color: #2d3748; border: 1px solid #e9ecef;">模型名称</th>
<th style="padding: 12px; text-align: left; font-weight: 600; color: #2d3748; border: 1px solid #e9ecef;">输入倍率</th>
<th style="padding: 12px; text-align: left; font-weight: 600; color: #2d3748; border: 1px solid #e9ecef;">输出倍率</th>
<th style="padding: 12px; text-align: center; font-weight: 600; color: #2d3748; border: 1px solid #e9ecef; width: 100px;">操作</th>
</tr>
</thead>
<tbody id="pricingTableBody">
<!-- 动态生成的定价行 -->
</tbody>
</table>
</div>
<div id="pricingJsonMode" style="display: none;">
<div class="form-group">
<label for="pricingConfigJson">定价配置 (JSON)</label>
<textarea id="pricingConfigJson" class="form-control" rows="15"
placeholder='{"gpt-3.5-turbo": {"input": 0.001, "output": 0.002}, "gpt-4": {"input": 0.03, "output": 0.06}}'></textarea>
<small style="color: #666; font-size: 12px; margin-top: 8px; display: block;">
配置格式:模型名称 -> {input: 输入倍率, output: 输出倍率}。倍率基于1百万token=$1.00计算
</small>
</div>
</div>
`;
}
} else {
showAlert(`加载定价配置失败: ${error.message}`, 'error');
}
}
}
// 显示定价配置表格
function displayPricingTable(pricing) {
const tbody = DOMUtils.getElement('pricingTableBody');
if (!tbody) return;
tbody.innerHTML = '';
// 如果没有定价数据,添加一行空白行
if (!pricing || Object.keys(pricing).length === 0) {
addPricingModel();
return;
}
// 为每个模型创建一行
Object.entries(pricing).forEach(([modelName, modelPricing]) => {
const row = document.createElement('tr');
row.style.borderBottom = '1px solid #e9ecef';
const inputPrice = modelPricing.input || 0;
const outputPrice = modelPricing.output || 0;
row.innerHTML = `
<td style="padding: 12px; border: 1px solid #e9ecef;">
<input type="text" class="form-control" value="${modelName}"
style="border: none; padding: 8px; background: transparent;">
</td>
<td style="padding: 12px; border: 1px solid #e9ecef;">
<input type="number" class="form-control" value="${inputPrice}" step="0.001" min="0"
style="border: none; padding: 8px; background: transparent;">
</td>
<td style="padding: 12px; border: 1px solid #e9ecef;">
<input type="number" class="form-control" value="${outputPrice}" step="0.001" min="0"
style="border: none; padding: 8px; background: transparent;">
</td>
<td style="padding: 12px; border: 1px solid #e9ecef; text-align: center;">
<button class="btn btn-danger" onclick="removePricingRow(this)" style="font-size: 12px; padding: 6px 12px; white-space: nowrap;">🗑️ 删除</button>
</td>
`;
tbody.appendChild(row);
});
}
// 定价验证类
class PricingValidator {
static validateTableData() {
const tbody = DOMUtils.getElement('pricingTableBody');
if (!tbody) {
return { isValid: false, error: '定价表格未找到,请刷新页面重试' };
}
const config = {};
let errorMessage = '';
// 遍历所有行收集数据
const rows = Array.from(tbody.children);
for (let index = 0; index < rows.length; index++) {
const row = rows[index];
const inputs = row.querySelectorAll('input');
if (inputs.length < 3) continue;
const modelName = inputs[0].value.trim();
const inputPrice = parseFloat(inputs[1].value);
const outputPrice = parseFloat(inputs[2].value);
// 验证数据
if (!modelName) {
errorMessage = `第 ${index + 1} 行:模型名称不能为空`;
break;
}
if (isNaN(inputPrice) || inputPrice < 0) {
errorMessage = `第 ${index + 1} 行:输入倍率必须是有效的非负数`;
break;
}
if (isNaN(outputPrice) || outputPrice < 0) {
errorMessage = `第 ${index + 1} 行:输出倍率必须是有效的非负数`;
break;
}
// 检查模型名称是否重复
if (config[modelName]) {
errorMessage = `模型名称 "${modelName}" 重复`;
break;
}
config[modelName] = {
input: inputPrice,
output: outputPrice
};
}
if (errorMessage) {
return { isValid: false, error: errorMessage };
}
if (Object.keys(config).length === 0) {
return { isValid: false, error: '请至少添加一个模型的定价配置' };
}
return { isValid: true, data: config };
}
}
async function updatePricing() {
let config;
if (pricingEditMode === 'table') {
const validation = PricingValidator.validateTableData();
if (!validation.isValid) {
showAlert(validation.error, 'error');
return;
}
config = validation.data;
} else {
const configText = DOMUtils.getValue('pricingConfigJson');
if (!configText) {
showAlert('请填写定价配置', 'error');
return;
}
config = FormUtils.validateJSON(configText, '配置格式错误,请检查JSON格式');
if (!config) return;
}
try {
await apiRequest('/api/admin/pricing', 'POST', config, true, '正在更新定价配置...');
showAlert('定价配置更新成功!', 'success');
loadPricing();
} catch (error) {
showAlert(`更新定价配置失败: ${error.message}`, 'error');
}
}
// === API TEST FUNCTIONS ===
// API测试管理类
class ApiTestManager {
static async sendRequest() {
const token = DOMUtils.getValue('apiTestToken');
const requestText = DOMUtils.getValue('apiTestRequest');
const responseTextarea = DOMUtils.getElement('apiTestResponse');
const statusDiv = DOMUtils.getElement('apiTestStatus');
if (!FormUtils.validateRequired([
{ id: 'apiTestToken', name: 'Bearer Token', message: '请输入 Bearer Token' },
{ id: 'apiTestRequest', name: '请求 JSON', message: '请输入请求 JSON' }
])) {
return;
}
const requestJson = FormUtils.validateJSON(requestText, '请求 JSON 格式错误');
if (!requestJson) return;
// 显示加载状态
if (responseTextarea) responseTextarea.value = '正在发送请求...';
if (statusDiv) statusDiv.style.display = 'none';
try {
const startTime = Date.now();
const response = await fetch('/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(requestJson)
});
const endTime = Date.now();
const duration = endTime - startTime;
this._displayTestResult(response, duration, responseTextarea, statusDiv);
} catch (error) {
this._displayTestError(error, responseTextarea, statusDiv);
}
}
static async _displayTestResult(response, duration, responseTextarea, statusDiv) {
if (statusDiv) statusDiv.style.display = 'block';
if (response.ok) {
const responseData = await response.json();
if (responseTextarea) {
responseTextarea.value = JSON.stringify(responseData, null, 2);
}
if (statusDiv) {
statusDiv.className = 'alert alert-success';
statusDiv.innerHTML = `
<strong>✅ 请求成功</strong><br>
状态码: ${response.status}<br>
响应时间: ${duration}ms
`;
}
} else {
const errorText = await response.text();
if (responseTextarea) responseTextarea.value = errorText;
if (statusDiv) {
statusDiv.className = 'alert alert-error';
statusDiv.innerHTML = `
<strong>❌ 请求失败</strong><br>
状态码: ${response.status}<br>
响应时间: ${duration}ms
`;
}
}
}
static _displayTestError(error, responseTextarea, statusDiv) {
if (responseTextarea) {
responseTextarea.value = `请求失败: ${error.message}`;
}
if (statusDiv) {
statusDiv.style.display = 'block';
statusDiv.className = 'alert alert-error';
statusDiv.innerHTML = `<strong>❌ 网络错误</strong><br>${error.message}`;
}
}
static clear() {
DOMUtils.clearInput('apiTestToken');
DOMUtils.setValue('apiTestRequest', `{
"model": "gpt-4",
"messages": [
{
"role": "user",
"content": "Hello, world!"
}
]
}`);
DOMUtils.clearInput('apiTestResponse');
const statusDiv = DOMUtils.getElement('apiTestStatus');
if (statusDiv) statusDiv.style.display = 'none';
}
}
// 发送 API 测试请求
async function sendApiTest() {
await ApiTestManager.sendRequest();
}
// 清空 API 测试数据
function clearApiTest() {
ApiTestManager.clear();
}
// === UTILITY FUNCTIONS ===
// 剪贴板管理类
class ClipboardManager {
static async copy(text) {
try {
await navigator.clipboard.writeText(text);
showAlert('已复制到剪贴板', 'success', 2000);
} catch (error) {
// 降级方案
this._fallbackCopy(text);
}
}
static _fallbackCopy(text) {
const textArea = document.createElement('textarea');
textArea.value = text;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
showAlert('已复制到剪贴板', 'success', 2000);
}
}
function copyToClipboard(text) {
ClipboardManager.copy(text);
}
// 消息提示管理类
class AlertManager {
static show(message, type, duration = 5000) {
// 移除现有提示
this._removeExistingAlerts();
const alertDiv = document.createElement('div');
alertDiv.className = `alert alert-${type} alert-top`;
alertDiv.textContent = message;
const container = document.querySelector('.container');
if (container) {
container.insertBefore(alertDiv, container.firstChild);
}
// 自动移除
setTimeout(() => {
alertDiv.remove();
}, duration);
}
static _removeExistingAlerts() {
const existingAlerts = document.querySelectorAll('.alert');
existingAlerts.forEach(alert => alert.remove());
}
}
function showAlert(message, type, duration = 5000) {
AlertManager.show(message, type, duration);
}
// Loading 管理对象
const LoadingManager = {
delayTimer: null,
minShowTimer: null,
loadingElement: null,
showStartTime: null,
showDelay: 500, // 延迟 500ms 显示,避免快速请求的闪烁
minShowDuration: 500, // 最小显示时长 500ms,避免一闪而过
show(message) {
// 如果已存在 loading,先清理
this.hide();
// 设置延迟显示
this.delayTimer = setTimeout(() => {
this._showLoading(message);
}, this.showDelay);
},
_showLoading(message) {
const mainContent = document.getElementById('mainContent');
if (!mainContent) return;
// 记录显示开始时间
this.showStartTime = Date.now();
// 添加 loading 状态类
mainContent.classList.add('loading-active');
// 创建 loading 元素
const loadingDiv = document.createElement('div');
loadingDiv.id = 'loadingOverlay';
loadingDiv.innerHTML = `
<div class="spinner"></div>
<p>${message}</p>
`;
// 初始设置为透明,准备淡入动画
loadingDiv.style.opacity = '0';
mainContent.appendChild(loadingDiv);
this.loadingElement = loadingDiv;
// 触发淡入动画
requestAnimationFrame(() => {
loadingDiv.style.opacity = '1';
});
},
hide() {
// 清除延迟显示定时器
if (this.delayTimer) {
clearTimeout(this.delayTimer);
this.delayTimer = null;
}
// 如果 loading 还没有显示,直接返回
if (!this.loadingElement) {
return;
}
// 计算已显示时长
const showDuration = Date.now() - this.showStartTime;
const remainingMinShowTime = Math.max(0, this.minShowDuration - showDuration);
// 如果还没达到最小显示时长,延迟隐藏
if (remainingMinShowTime > 0) {
this.minShowTimer = setTimeout(() => {
this._hideLoading();
}, remainingMinShowTime);
} else {
this._hideLoading();
}
},
_hideLoading() {
// 清除最小显示定时器
if (this.minShowTimer) {
clearTimeout(this.minShowTimer);
this.minShowTimer = null;
}
if (this.loadingElement) {
// 淡出动画
this.loadingElement.style.opacity = '0';
// 等待动画完成后移除元素
setTimeout(() => {
if (this.loadingElement) {
this.loadingElement.remove();
this.loadingElement = null;
}
// 移除 loading 状态类
const mainContent = document.getElementById('mainContent');
if (mainContent) {
mainContent.classList.remove('loading-active');
}
}, 300); // 与 CSS transition 时间匹配
}
this.showStartTime = null;
}
};
// 保持原有的函数接口,以兼容现有代码
function showLoading(message) {
LoadingManager.show(message);
}
function hideLoading() {
LoadingManager.hide();
}
// === 页面初始化和事件处理 ===
// 页面初始化管理类
class PageInitializer {
static init() {
this._initAuth();
this._initEventListeners();
this._initKeyboardShortcuts();
}
static _initAuth() {
if (AuthManager.restoreToken()) {
showMainContent();
} else {
showHomePage();
}
}
static _initEventListeners() {
// 为登录输入框添加回车键监听
const tokenInput = DOMUtils.getElement('adminToken');
if (tokenInput) {
tokenInput.addEventListener('keypress', (event) => {
if (event.key === 'Enter') {
authenticate();
}
});
}
// 点击模态框外部关闭
window.onclick = (event) => {
const modal = DOMUtils.getElement('loginModal');
if (event.target === modal) {
closeLoginModal();
}
};
}
static _initKeyboardShortcuts() {
// ESC键关闭模态框
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
const modal = DOMUtils.getElement('loginModal');
if (modal && modal.style.display === 'block') {
closeLoginModal();
}
}
});
}
}
// Initialize on page load
document.addEventListener('DOMContentLoaded', () => {
PageInitializer.init();
});
</script>
</body>
</html>