Files
one-api-workers/public/index.html
T
dreamhunter2333andClaude Sonnet 4.5 198dc500aa feat: 重构管理界面并添加统一 loading 效果
- 完全重构前端管理界面,采用模块化架构
- 新增独立的 CSS 和 JS 模块(utils, auth, channels, tokens, pricing, api-test, database)
- 实现统一的 loading 遮罩效果,所有 API 请求自动显示加载状态
- 添加现代化多选下拉框组件用于频道选择
- 实现模型部署映射的可视化表格编辑
- 修复页面高度自适应问题,管理页面占满全屏
- 优化侧边栏布局,添加 GitHub 链接和版本号
- 令牌管理页面隐藏敏感 key,添加复制功能
- 管理页面切换时自动重置为列表视图并重新加载数据
- 新增 Claude Proxy 提供商支持
- 统一所有 API 调用的错误处理和 loading 状态管理

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-17 18:41:05 +08:00

591 lines
35 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>">
<link rel="stylesheet" href="/css/styles.css">
</head>
<body>
<div class="app-container">
<!-- Sidebar -->
<aside class="sidebar" id="sidebar">
<div class="sidebar-header">
<div class="sidebar-brand">
<span>🤖</span>
<span>AWSL One API</span>
</div>
</div>
<nav class="sidebar-nav">
<div class="nav-section">
<div class="nav-section-title">主菜单</div>
<button class="nav-item active" onclick="switchTab('dashboard')" data-tab="dashboard">
<span class="nav-item-icon">📊</span>
<span>仪表盘</span>
</button>
<button class="nav-item" onclick="switchTab('api-test')" data-tab="api-test">
<span class="nav-item-icon">🧪</span>
<span>API 测试</span>
</button>
</div>
<div class="nav-section admin-only hidden">
<div class="nav-section-title">管理</div>
<button class="nav-item" onclick="switchTab('channels')" data-tab="channels">
<span class="nav-item-icon">🔗</span>
<span>频道管理</span>
</button>
<button class="nav-item" onclick="switchTab('tokens')" data-tab="tokens">
<span class="nav-item-icon">🔑</span>
<span>令牌管理</span>
</button>
<button class="nav-item" onclick="switchTab('pricing')" data-tab="pricing">
<span class="nav-item-icon">💰</span>
<span>定价管理</span>
</button>
</div>
<div class="nav-section admin-only hidden">
<div class="nav-section-title">系统</div>
<button class="nav-item" onclick="switchTab('database')" data-tab="database">
<span class="nav-item-icon">🗄️</span>
<span>数据库</span>
</button>
</div>
</nav>
<div class="sidebar-footer">
<div style="padding: 0.75rem 1rem; border-top: 1px solid hsl(var(--border)); display: flex; justify-content: space-between; align-items: center; font-size: 0.875rem; color: hsl(var(--muted-foreground)); margin-bottom: 0.75rem;">
<a href="https://github.com/dreamhunter2333/awsl-one-api" target="_blank" style="color: hsl(var(--primary)); text-decoration: none; display: flex; align-items: center; gap: 0.25rem; transition: opacity 0.2s;">
🔗 GitHub
</a>
<span>v1.0.0</span>
</div>
<button class="btn btn-outline w-full" onclick="toggleTheme()" style="justify-content: center; margin-bottom: 0.5rem;">
<span id="themeIcon">🌙</span>
<span style="margin-left: 0.5rem;">切换主题</span>
</button>
<button class="btn btn-outline w-full" onclick="toggleAuthModal()" id="authButton">
🔑 管理员登录
</button>
</div>
</aside>
<!-- Main Content -->
<main class="main-content">
<header class="main-header">
<div class="header-content">
<h1 class="header-title" id="pageTitle">仪表盘</h1>
</div>
</header>
<div class="content-wrapper">
<!-- Dashboard Tab -->
<div id="dashboard" class="tab-content active">
<div class="alert alert-info mb-6" id="loginPrompt">
<div class="flex items-center justify-between">
<div>
<strong>💡 提示</strong>
<p class="text-sm mt-1">要使用管理功能,请先进行身份验证</p>
</div>
<button class="btn btn-primary btn-sm" onclick="toggleAuthModal()">
立即登录
</button>
</div>
</div>
<div class="card mb-6">
<div class="card-header">
<h2 class="card-title">欢迎使用 AWSL One API</h2>
<p class="card-description">统一的 AI 模型接口管理服务</p>
</div>
<div class="card-content">
<p class="mb-6">
AWSL One API 是一个强大的 API 代理和管理平台,为您提供统一的 AI 模型接口管理服务。
</p>
<h3 class="font-semibold mb-4">核心特性</h3>
<div class="grid grid-cols-2 gap-4">
<div class="card">
<div class="card-content">
<h4 class="font-semibold mb-2">🌐 多提供商支持</h4>
<p class="text-sm text-muted">支持 OpenAI、Azure OpenAI 和 Claude API,轻松切换不同的 AI 服务。</p>
</div>
</div>
<div class="card">
<div class="card-content">
<h4 class="font-semibold mb-2">⚖️ 负载均衡</h4>
<p class="text-sm text-muted">自动在多个频道间分配请求,确保服务稳定性和高可用性。</p>
</div>
</div>
<div class="card">
<div class="card-content">
<h4 class="font-semibold mb-2">📊 用量追踪</h4>
<p class="text-sm text-muted">实时监控 token 使用情况,精确计算成本,防止超出配额。</p>
</div>
</div>
<div class="card">
<div class="card-content">
<h4 class="font-semibold mb-2">🔐 安全管理</h4>
<p class="text-sm text-muted">基于令牌的访问控制,为不同用户分配独立的 API 密钥。</p>
</div>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title">快速开始</h3>
<p class="card-description">按照以下步骤开始使用</p>
</div>
<div class="card-content">
<ol style="list-style: decimal; padding-left: 1.5rem; line-height: 2;">
<li><strong>管理员登录:</strong>使用管理员令牌登录系统</li>
<li><strong>配置频道:</strong>添加 OpenAI、Azure 或 Claude API 频道</li>
<li><strong>创建令牌:</strong>为您的应用创建 API 令牌</li>
<li><strong>开始使用:</strong>使用生成的令牌调用 API</li>
</ol>
</div>
</div>
</div>
<!-- API Test Tab -->
<div id="api-test" class="tab-content">
<div class="card">
<div class="card-header">
<h2 class="card-title">API 测试</h2>
<p class="card-description">测试您的 API 连接和配置</p>
</div>
<div class="card-content">
<form id="apiTestForm" onsubmit="testAPI(event)">
<div class="grid grid-cols-2">
<div class="form-group">
<label class="label">API 端点</label>
<select class="input" id="testEndpoint" onchange="updateRequestTemplate()">
<option value="/v1/chat/completions">/v1/chat/completions</option>
<option value="/v1/messages">/v1/messages</option>
<option value="/v1/completions">/v1/completions</option>
</select>
</div>
<div class="form-group">
<label class="label">API 令牌</label>
<input type="text" class="input" id="testApiToken" placeholder="sk-..." required>
</div>
</div>
<div class="form-group">
<label class="label">请求体 (JSON)</label>
<textarea class="input" id="testRequestBody" rows="12" placeholder='{"model": "gpt-3.5-turbo", "messages": [...]}'></textarea>
<p class="text-sm text-muted mt-1">直接编辑 JSON 格式的请求体</p>
</div>
<button type="submit" class="btn btn-primary" id="testSubmitBtn">
🚀 发送请求
</button>
</form>
<div id="testResult" class="hidden" style="margin-top: 1.5rem;">
<div class="flex items-center justify-between mb-3">
<h3 class="font-semibold">响应结果</h3>
<div class="flex gap-2">
<span class="badge badge-secondary" id="testStatusBadge">等待中</span>
<span class="badge badge-outline" id="testTimeBadge">0ms</span>
</div>
</div>
<div class="card">
<div class="card-content">
<pre id="testResultContent" style="white-space: pre-wrap; word-wrap: break-word; font-size: 0.875rem; line-height: 1.6; max-height: 500px; overflow-y: auto;"></pre>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Channels Tab -->
<div id="channels" class="tab-content">
<!-- Channel List View -->
<div id="channelListView">
<div class="card">
<div class="card-header">
<div class="flex gap-2">
<button class="btn btn-primary" 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 class="flex gap-2">
<button class="btn btn-secondary" onclick="toggleChannelEditMode()" id="channelModeToggle">📝 切换到JSON模式</button>
<button class="btn btn-primary" onclick="saveChannel()">💾 保存频道</button>
<button class="btn btn-secondary" onclick="showChannelList()">← 返回列表</button>
</div>
</div>
<div class="card-content">
<!-- Common Key Field -->
<div class="form-group">
<label for="channelKey">频道标识</label>
<input type="text" id="channelKey" class="input" placeholder="例如: azure-openai-1">
</div>
<!-- Form Mode -->
<div id="channelFormMode">
<div class="form-group">
<label for="channelName">频道名称</label>
<input type="text" id="channelName" class="input" placeholder="例如: Azure OpenAI">
</div>
<div class="form-group">
<label for="channelType">频道类型</label>
<select id="channelType" class="input">
<option value="azure-openai">Azure OpenAI</option>
<option value="openai">OpenAI</option>
<option value="claude">Claude</option>
</select>
</div>
<div class="form-group">
<label for="channelEndpoint">API 端点</label>
<input type="text" id="channelEndpoint" class="input" placeholder="https://your-resource.openai.azure.com/">
</div>
<div class="form-group">
<label for="channelApiKey">API 密钥</label>
<input type="password" id="channelApiKey" class="input" placeholder="your-api-key">
</div>
<div class="form-group">
<label for="channelApiVersion">API 版本(可选)</label>
<input type="text" id="channelApiVersion" class="input" placeholder="例如: 2024-02-01">
</div>
<div class="form-group">
<label for="channelMapperTable">模型部署映射</label>
<div style="margin-bottom: 0.5rem;">
<button type="button" class="btn btn-secondary btn-sm" onclick="window.addMapperRow()">➕ 添加映射</button>
</div>
<div class="table-container">
<table class="table">
<thead>
<tr>
<th>请求模型名</th>
<th>部署模型名</th>
<th style="width: 100px; text-align: center;">操作</th>
</tr>
</thead>
<tbody id="channelMapperTable">
<tr>
<td colspan="3" class="text-center text-muted">暂无映射,点击上方按钮添加</td>
</tr>
</tbody>
</table>
</div>
<p class="text-sm text-muted mt-1">配置模型名称映射关系(主要用于Azure OpenAI)。留空表示不进行映射</p>
</div>
</div>
<!-- JSON Mode -->
<div id="channelJsonMode" style="display: none;">
<div class="form-group">
<label for="channelValueJson">频道配置 (JSON)</label>
<textarea id="channelValueJson" class="input" 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>
<p class="text-sm text-muted mt-1">直接编辑完整的频道配置JSON</p>
</div>
</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">
<div class="flex gap-2">
<button class="btn btn-primary" 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 class="flex gap-2">
<button class="btn btn-secondary" onclick="toggleTokenEditMode()" id="tokenModeToggle">📝 切换到JSON模式</button>
<button class="btn btn-primary" onclick="saveToken()">💾 保存令牌</button>
<button class="btn btn-secondary" onclick="showTokenList()">← 返回列表</button>
</div>
</div>
<div class="card-content">
<!-- Common Key Field -->
<div class="form-group">
<label for="tokenKey">令牌标识</label>
<div class="flex gap-2">
<input type="text" id="tokenKey" class="input" placeholder="输入令牌标识或点击生成">
<button type="button" id="generateTokenBtn" class="btn btn-secondary" onclick="generateTokenKey()">🎲 生成</button>
</div>
</div>
<!-- Form Mode -->
<div id="tokenFormMode">
<div class="form-group">
<label for="tokenName">令牌名称</label>
<input type="text" id="tokenName" class="input" placeholder="例如: 用户令牌1">
</div>
<div class="form-group">
<label for="channelKeysSelect">允许访问的频道</label>
<div class="multi-select-wrapper">
<div class="multi-select-trigger" id="channelSelectTrigger" onclick="window.toggleChannelDropdown()">
<span class="multi-select-value" id="channelSelectValue">请选择频道...</span>
<span class="multi-select-arrow">▼</span>
</div>
<div class="multi-select-dropdown hidden" id="channelSelectDropdown">
<div class="multi-select-search">
<input type="text" class="input" placeholder="搜索频道..." id="channelSearchInput" oninput="window.filterChannels()">
</div>
<div class="multi-select-options" id="channelOptionsContainer">
<div class="multi-select-loading">加载中...</div>
</div>
</div>
</div>
<p class="text-sm text-muted mt-1">不选表示允许访问所有频道</p>
</div>
<div class="form-group">
<label for="totalQuotaInput">总配额</label>
<input type="number" id="totalQuotaInput" class="input" placeholder="1000000" min="0" step="1">
<div style="margin-top: 10px;">
<small class="text-sm text-muted" style="display: block; margin-bottom: 8px;">快速选择:</small>
<div style="display: flex; gap: 5px; flex-wrap: wrap;">
<button type="button" class="btn btn-secondary btn-sm" onclick="setQuotaValue(1000000)">$1</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="setQuotaValue(5000000)">$5</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="setQuotaValue(10000000)">$10</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="setQuotaValue(20000000)">$20</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="setQuotaValue(50000000)">$50</button>
</div>
</div>
<p class="text-sm text-muted mt-2">设置令牌的总使用配额 (基础单位:1百万token = $1.00,例如:输入1000000表示1美元的配额)</p>
</div>
</div>
<!-- JSON Mode -->
<div id="tokenJsonMode" style="display: none;">
<div class="form-group">
<label for="tokenValueJson">令牌配置 (JSON)</label>
<textarea id="tokenValueJson" class="input" rows="10" placeholder='{"name": "用户令牌1", "channel_keys": ["azure-openai-1"], "total_quota": 1000000}'></textarea>
<p class="text-sm text-muted mt-1">直接编辑完整的令牌配置JSON</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Pricing Tab -->
<div id="pricing" class="tab-content">
<div class="card">
<div class="card-header">
<div class="flex gap-2">
<button class="btn btn-secondary" onclick="togglePricingEditMode()" id="pricingModeToggle">📝 切换到JSON模式</button>
<button class="btn btn-secondary" onclick="addPricingModel()" id="addPricingModelBtn">➕ 添加模型</button>
<button class="btn btn-primary" onclick="savePricing(event)">💾 更新定价</button>
<button class="btn btn-secondary" onclick="loadPricing()">🔄 刷新</button>
</div>
</div>
<div class="card-content">
<div style="background: hsl(var(--accent)); padding: 15px; border-radius: 6px; margin-bottom: 20px; border-left: 4px solid hsl(var(--primary));">
<h4 style="margin: 0 0 10px 0; color: hsl(var(--foreground)); font-size: 14px; font-weight: 600;">💡 定价说明</h4>
<p style="margin: 0; font-size: 13px; color: hsl(var(--foreground)); line-height: 1.5;">
<strong>基础配额单位:1百万token = $1.00</strong> | 定价配置为不同模型相对于基础单位的倍率 | 例如:gpt-4的input倍率为30,表示1百万token消耗30美元配额
</p>
</div>
<!-- Table Mode -->
<div id="pricingTableMode">
<div class="table-container">
<table class="table">
<thead>
<tr>
<th>模型名称</th>
<th>输入倍率</th>
<th>输出倍率</th>
<th style="width: 100px; text-align: center;">操作</th>
</tr>
</thead>
<tbody id="pricingTableBody">
<tr>
<td colspan="4" class="text-center text-muted">加载中...</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- JSON Mode -->
<div id="pricingJsonMode" style="display: none;">
<div class="form-group">
<label for="pricingConfigJson">定价配置 (JSON)</label>
<textarea class="input" id="pricingConfigJson" rows="15" placeholder='{"gpt-3.5-turbo": {"input": 0.001, "output": 0.002}, "gpt-4": {"input": 0.03, "output": 0.06}}'></textarea>
<p class="text-sm text-muted mt-2">配置格式:模型名称 -> {input: 输入倍率, output: 输出倍率}。倍率基于1百万token=$1.00计算</p>
</div>
</div>
</div>
</div>
</div>
<!-- Database Tab -->
<div id="database" class="tab-content">
<div class="card">
<div class="card-content">
<div class="alert alert-warning mb-4">
<strong>⚠️ 警告:</strong>
<p class="text-sm mt-1">初始化数据库将创建必要的表结构。如果表已存在,操作将被忽略。</p>
</div>
<button class="btn btn-primary" onclick="initDatabase()">
🗄️ 初始化数据库
</button>
<div id="dbResult" class="hidden" style="margin-top: 1.5rem;">
<div class="card">
<div class="card-content">
<pre id="dbResultContent" style="white-space: pre-wrap; font-size: 0.875rem;"></pre>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
</div>
<!-- Auth Modal -->
<div class="modal-overlay" id="authModal">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title">管理员身份验证</h2>
</div>
<div class="modal-body">
<form id="authForm" onsubmit="authenticate(event)">
<div class="form-group">
<label class="label">管理员令牌</label>
<input type="password" class="input" id="adminToken" placeholder="请输入管理员令牌" required>
</div>
<div id="authError" class="alert alert-error hidden">
<strong>❌ 错误:</strong>
<p class="text-sm mt-1" id="authErrorMessage"></p>
</div>
</form>
</div>
<div class="modal-footer">
<button class="btn btn-outline" onclick="toggleAuthModal()">取消</button>
<button class="btn btn-primary" onclick="authenticate(event)">登录</button>
</div>
</div>
</div>
<!-- Add Channel Modal -->
<div class="modal-overlay" id="addChannelModal">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title">添加频道</h2>
</div>
<div class="modal-body">
<form id="addChannelForm">
<div class="form-group">
<label class="label">频道 Key</label>
<input type="text" class="input" id="channelKey" placeholder="channel-1" required>
</div>
<div class="form-group">
<label class="label">频道名称</label>
<input type="text" class="input" id="channelName" placeholder="My Channel" required>
</div>
<div class="form-group">
<label class="label">类型</label>
<select class="input" id="channelType" required>
<option value="openai">OpenAI</option>
<option value="azure-openai">Azure OpenAI</option>
<option value="claude">Claude</option>
</select>
</div>
<div class="form-group">
<label class="label">API 端点</label>
<input type="url" class="input" id="channelEndpoint" placeholder="https://api.openai.com" required>
</div>
<div class="form-group">
<label class="label">API 密钥</label>
<input type="password" class="input" id="channelApiKey" placeholder="sk-..." required>
</div>
<div class="form-group">
<label class="label">模型映射 (JSON)</label>
<textarea class="input" id="channelMapper" rows="4" placeholder='{"gpt-3.5-turbo": "gpt-3.5-turbo"}' required></textarea>
</div>
</form>
</div>
<div class="modal-footer">
<button class="btn btn-outline" onclick="toggleModal('addChannelModal')">取消</button>
<button class="btn btn-primary" onclick="addChannel(event)">添加</button>
</div>
</div>
</div>
<!-- Add Token Modal -->
<div class="modal-overlay" id="addTokenModal">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title">创建令牌</h2>
</div>
<div class="modal-body">
<form id="addTokenForm">
<div class="form-group">
<label class="label">令牌 Key</label>
<input type="text" class="input" id="tokenKey" placeholder="sk-..." required>
</div>
<div class="form-group">
<label class="label">令牌名称</label>
<input type="text" class="input" id="tokenName" placeholder="My Token" required>
</div>
<div class="form-group">
<label class="label">总配额</label>
<input type="number" class="input" id="tokenQuota" placeholder="100" step="0.01" required>
</div>
<div class="form-group">
<label class="label">允许的频道 (留空允许所有)</label>
<input type="text" class="input" id="tokenChannels" placeholder="channel-1,channel-2">
<p class="text-sm text-muted mt-1">多个频道用逗号分隔</p>
</div>
</form>
</div>
<div class="modal-footer">
<button class="btn btn-outline" onclick="toggleModal('addTokenModal')">取消</button>
<button class="btn btn-primary" onclick="addToken(event)">创建</button>
</div>
</div>
</div>
<!-- Loading Overlay -->
<div id="loadingOverlay">
<div class="loading-spinner"></div>
<div class="loading-text">加载中...</div>
</div>
<script type="module" src="/js/main.js"></script>
</body>
</html>