Files
claude-relay-service/web/admin/index.html
shaw 2f4730baba 优化: 替换第三方CDN资源以提升加载速度
- 将所有第三方资源从 bootcdn 迁移到 cdnjs.cloudflare.com
- 移除 SRI 完整性校验以避免哈希值不匹配问题
- 添加 DNS 预取和预连接以加速资源加载
- 调整脚本加载顺序,确保依赖关系正确
- 保持所有库版本号不变 (Vue 3.3.4, Element Plus 2.4.4, Chart.js 4.4.0)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-23 11:15:33 +08:00

3411 lines
235 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>Claude Relay Service - 管理后台</title>
<!-- 预连接到CDN域名加速资源加载 -->
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin>
<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin>
<link rel="dns-prefetch" href="https://cdn.jsdelivr.net">
<link rel="dns-prefetch" href="https://cdnjs.cloudflare.com">
<!-- 使用更快的CDN资源保持版本一致 -->
<!-- Vue 3.3.4 (必须先加载不使用defer) -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/3.3.4/vue.global.prod.min.js" crossorigin="anonymous"></script>
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Chart.js 4.4.0 (独立库,可以延迟加载) -->
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.0/chart.umd.js" crossorigin="anonymous"></script>
<!-- Element Plus 2.4.4 (依赖Vue所以在Vue之后加载) -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/element-plus/2.4.4/index.min.css" crossorigin="anonymous">
<script src="https://cdnjs.cloudflare.com/ajax/libs/element-plus/2.4.4/index.full.min.js" crossorigin="anonymous"></script>
<!-- Element Plus 中文语言包 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/element-plus/2.4.4/locale/zh-cn.min.js" crossorigin="anonymous"></script>
<!-- Font Awesome 6.5.1 -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" integrity="sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<link rel="stylesheet" href="/web/style.css">
</head>
<body>
<div id="app" v-cloak>
<!-- 登录界面 -->
<div v-if="!isLoggedIn" class="flex items-center justify-center min-h-screen p-6">
<div class="glass-strong rounded-3xl p-10 w-full max-w-md shadow-2xl">
<div class="text-center mb-8">
<div class="w-20 h-20 mx-auto mb-6 bg-gradient-to-br from-blue-500/20 to-purple-500/20 border border-gray-300/30 rounded-2xl flex items-center justify-center backdrop-blur-sm">
<i class="fas fa-cloud text-3xl text-gray-700"></i>
</div>
<h1 class="text-3xl font-bold text-white mb-2 header-title">Claude Relay Service</h1>
<p class="text-white/80 text-lg">管理后台</p>
</div>
<form @submit.prevent="login" class="space-y-6">
<div>
<label class="block text-sm font-semibold text-gray-900 mb-3">用户名</label>
<input
v-model="loginForm.username"
type="text"
required
class="form-input w-full"
placeholder="请输入用户名"
>
</div>
<div>
<label class="block text-sm font-semibold text-gray-900 mb-3">密码</label>
<input
v-model="loginForm.password"
type="password"
required
class="form-input w-full"
placeholder="请输入密码"
>
</div>
<button
type="submit"
:disabled="loginLoading"
class="btn btn-primary w-full py-4 px-6 text-lg font-semibold"
>
<i v-if="!loginLoading" class="fas fa-sign-in-alt mr-2"></i>
<div v-if="loginLoading" class="loading-spinner mr-2"></div>
{{ loginLoading ? '登录中...' : '登录' }}
</button>
</form>
<div v-if="loginError" class="mt-6 p-4 bg-red-500/20 border border-red-500/30 rounded-xl text-red-800 text-sm text-center backdrop-blur-sm">
<i class="fas fa-exclamation-triangle mr-2"></i>{{ loginError }}
</div>
</div>
</div>
<!-- 管理界面 -->
<div v-if="isLoggedIn" class="min-h-screen p-6">
<!-- 顶部导航 -->
<div class="glass-strong rounded-3xl p-6 mb-8 shadow-xl" style="z-index: 10; position: relative;">
<div class="flex flex-col md:flex-row justify-between items-center gap-4">
<div class="flex items-center gap-4">
<div class="w-12 h-12 bg-gradient-to-br from-blue-500/20 to-purple-500/20 border border-gray-300/30 rounded-xl flex items-center justify-center backdrop-blur-sm flex-shrink-0">
<i class="fas fa-cloud text-xl text-gray-700"></i>
</div>
<div class="flex flex-col justify-center min-h-[48px]">
<div class="flex items-center gap-3">
<h1 class="text-2xl font-bold text-white header-title leading-tight">Claude Relay Service</h1>
<!-- 版本信息 -->
<div class="flex items-center gap-2">
<span class="text-sm text-gray-400 font-mono">v{{ versionInfo.current || '...' }}</span>
<!-- 更新提示 -->
<a
v-if="versionInfo.hasUpdate"
:href="versionInfo.releaseInfo?.htmlUrl || '#'"
target="_blank"
class="inline-flex items-center gap-1 px-2 py-0.5 bg-green-500 border border-green-600 rounded-full text-xs text-white hover:bg-green-600 transition-colors animate-pulse"
title="有新版本可用"
>
<i class="fas fa-arrow-up text-[10px]"></i>
<span>新版本</span>
</a>
</div>
</div>
<p class="text-gray-600 text-sm leading-tight mt-0.5">管理后台</p>
</div>
</div>
<!-- 用户菜单 -->
<div class="relative user-menu-container">
<button
@click="userMenuOpen = !userMenuOpen"
class="btn btn-primary px-4 py-3 flex items-center gap-2 relative"
>
<i class="fas fa-user-circle"></i>
<span>{{ currentUser.username || 'Admin' }}</span>
<i class="fas fa-chevron-down text-xs transition-transform duration-200" :class="{ 'rotate-180': userMenuOpen }"></i>
</button>
<!-- 悬浮菜单 -->
<div
v-if="userMenuOpen"
class="absolute right-0 top-full mt-2 w-56 bg-white rounded-xl shadow-xl border border-gray-200 py-2 user-menu-dropdown"
style="z-index: 999999;"
@click.stop
>
<!-- 版本信息 -->
<div class="px-4 py-3 border-b border-gray-100">
<div class="flex items-center justify-between text-sm">
<span class="text-gray-500">当前版本</span>
<span class="font-mono text-gray-700">v{{ versionInfo.current || '...' }}</span>
</div>
<div v-if="versionInfo.hasUpdate" class="mt-2">
<div class="flex items-center justify-between text-sm mb-2">
<span class="text-green-600 font-medium">
<i class="fas fa-arrow-up mr-1"></i>有新版本
</span>
<span class="font-mono text-green-600">v{{ versionInfo.latest }}</span>
</div>
<a
:href="versionInfo.releaseInfo?.htmlUrl || '#'"
target="_blank"
class="block w-full text-center px-3 py-1.5 bg-green-500 text-white text-sm rounded-lg hover:bg-green-600 transition-colors"
>
<i class="fas fa-external-link-alt mr-1"></i>查看更新
</a>
</div>
<div v-else-if="versionInfo.checkingUpdate" class="mt-2 text-center text-xs text-gray-500">
<i class="fas fa-spinner fa-spin mr-1"></i>检查更新中...
</div>
<div v-else class="mt-2 text-center">
<!-- 已是最新版提醒 -->
<transition name="fade" mode="out-in">
<div v-if="versionInfo.noUpdateMessage" key="message" class="px-3 py-1.5 bg-green-100 border border-green-200 rounded-lg inline-block">
<p class="text-xs text-green-700 font-medium">
<i class="fas fa-check-circle mr-1"></i>当前已是最新版本
</p>
</div>
<button
v-else
key="button"
@click="checkForUpdates()"
class="text-xs text-blue-500 hover:text-blue-700 transition-colors"
>
<i class="fas fa-sync-alt mr-1"></i>检查更新
</button>
</transition>
</div>
</div>
<button
@click="openChangePasswordModal"
class="w-full px-4 py-3 text-left text-gray-700 hover:bg-gray-50 transition-colors flex items-center gap-3"
>
<i class="fas fa-key text-blue-500"></i>
<span>修改账户信息</span>
</button>
<hr class="my-2 border-gray-200">
<button
@click="logout"
class="w-full px-4 py-3 text-left text-red-600 hover:bg-red-50 transition-colors flex items-center gap-3"
>
<i class="fas fa-sign-out-alt"></i>
<span>退出登录</span>
</button>
</div>
</div>
</div>
</div>
<!-- 主内容区域 -->
<div class="glass-strong rounded-3xl p-6 shadow-xl" style="z-index: 1;">
<!-- 标签栏 -->
<div class="flex flex-wrap gap-2 mb-8 bg-white/10 rounded-2xl p-2 backdrop-blur-sm">
<button
v-for="tab in tabs"
:key="tab.key"
@click="activeTab = tab.key"
:class="['tab-btn flex-1 py-3 px-6 text-sm font-semibold transition-all duration-300',
activeTab === tab.key ? 'active' : 'text-gray-700 hover:bg-white/10 hover:text-gray-900']"
>
<i :class="tab.icon + ' mr-2'"></i>{{ tab.name }}
</button>
</div>
<!-- 仪表板 -->
<div v-if="activeTab === 'dashboard'" class="tab-content">
<!-- 主要统计 -->
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6 mb-8">
<div class="stat-card">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-semibold text-gray-600 mb-1">总API Keys</p>
<p class="text-3xl font-bold text-gray-900">{{ dashboardData.totalApiKeys }}</p>
<p class="text-xs text-gray-500 mt-1">活跃: {{ dashboardData.activeApiKeys || 0 }}</p>
</div>
<div class="stat-icon bg-gradient-to-br from-blue-500 to-blue-600">
<i class="fas fa-key"></i>
</div>
</div>
</div>
<div class="stat-card">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-semibold text-gray-600 mb-1">服务账户</p>
<p class="text-3xl font-bold text-gray-900">{{ dashboardData.totalAccounts }}</p>
<p class="text-xs text-gray-500 mt-1">
活跃: {{ dashboardData.activeAccounts || 0 }}
<span v-if="dashboardData.rateLimitedAccounts > 0" class="text-yellow-600">
| 限流: {{ dashboardData.rateLimitedAccounts }}
</span>
</p>
</div>
<div class="stat-icon bg-gradient-to-br from-green-500 to-green-600">
<i class="fas fa-user-circle"></i>
</div>
</div>
</div>
<div class="stat-card">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-semibold text-gray-600 mb-1">今日请求</p>
<p class="text-3xl font-bold text-gray-900">{{ dashboardData.todayRequests }}</p>
<p class="text-xs text-gray-500 mt-1">总请求: {{ formatNumber(dashboardData.totalRequests || 0) }}</p>
</div>
<div class="stat-icon bg-gradient-to-br from-purple-500 to-purple-600">
<i class="fas fa-chart-line"></i>
</div>
</div>
</div>
<div class="stat-card">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-semibold text-gray-600 mb-1">系统状态</p>
<p class="text-3xl font-bold text-green-600">{{ dashboardData.systemStatus }}</p>
<p class="text-xs text-gray-500 mt-1">运行时间: {{ formatUptime(dashboardData.uptime) }}</p>
</div>
<div class="stat-icon bg-gradient-to-br from-yellow-500 to-orange-500">
<i class="fas fa-heartbeat"></i>
</div>
</div>
</div>
</div>
<!-- Token统计和性能指标 -->
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6 mb-8">
<div class="stat-card">
<div class="flex items-center justify-between">
<div class="flex-1">
<p class="text-sm font-semibold text-gray-600 mb-1">今日Token</p>
<div class="flex items-baseline gap-2 mb-2">
<p class="text-3xl font-bold text-blue-600">{{ formatNumber((dashboardData.todayInputTokens || 0) + (dashboardData.todayOutputTokens || 0) + (dashboardData.todayCacheCreateTokens || 0) + (dashboardData.todayCacheReadTokens || 0)) }}</p>
<span class="text-sm text-green-600 font-medium">/ {{ costsData.todayCosts.formatted.totalCost }}</span>
</div>
<div class="text-xs text-gray-500">
<div class="flex justify-between items-center flex-wrap gap-x-4">
<span>输入: <span class="font-medium">{{ formatNumber(dashboardData.todayInputTokens || 0) }}</span></span>
<span>输出: <span class="font-medium">{{ formatNumber(dashboardData.todayOutputTokens || 0) }}</span></span>
<span v-if="(dashboardData.todayCacheCreateTokens || 0) > 0" class="text-purple-600">缓存创建: <span class="font-medium">{{ formatNumber(dashboardData.todayCacheCreateTokens || 0) }}</span></span>
<span v-if="(dashboardData.todayCacheReadTokens || 0) > 0" class="text-purple-600">缓存读取: <span class="font-medium">{{ formatNumber(dashboardData.todayCacheReadTokens || 0) }}</span></span>
</div>
</div>
</div>
<div class="stat-icon bg-gradient-to-br from-indigo-500 to-indigo-600">
<i class="fas fa-coins"></i>
</div>
</div>
</div>
<div class="stat-card">
<div class="flex items-center justify-between">
<div class="flex-1">
<p class="text-sm font-semibold text-gray-600 mb-1">总Token消耗</p>
<div class="flex items-baseline gap-2 mb-2">
<p class="text-3xl font-bold text-emerald-600">{{ formatNumber((dashboardData.totalInputTokens || 0) + (dashboardData.totalOutputTokens || 0) + (dashboardData.totalCacheCreateTokens || 0) + (dashboardData.totalCacheReadTokens || 0)) }}</p>
<span class="text-sm text-green-600 font-medium">/ {{ costsData.totalCosts.formatted.totalCost }}</span>
</div>
<div class="text-xs text-gray-500">
<div class="flex justify-between items-center flex-wrap gap-x-4">
<span>输入: <span class="font-medium">{{ formatNumber(dashboardData.totalInputTokens || 0) }}</span></span>
<span>输出: <span class="font-medium">{{ formatNumber(dashboardData.totalOutputTokens || 0) }}</span></span>
<span v-if="(dashboardData.totalCacheCreateTokens || 0) > 0" class="text-purple-600">缓存创建: <span class="font-medium">{{ formatNumber(dashboardData.totalCacheCreateTokens || 0) }}</span></span>
<span v-if="(dashboardData.totalCacheReadTokens || 0) > 0" class="text-purple-600">缓存读取: <span class="font-medium">{{ formatNumber(dashboardData.totalCacheReadTokens || 0) }}</span></span>
</div>
</div>
</div>
<div class="stat-icon bg-gradient-to-br from-emerald-500 to-emerald-600">
<i class="fas fa-database"></i>
</div>
</div>
</div>
<div class="stat-card">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-semibold text-gray-600 mb-1">平均RPM</p>
<p class="text-3xl font-bold text-orange-600">{{ dashboardData.systemRPM || 0 }}</p>
<p class="text-xs text-gray-500 mt-1">每分钟请求数</p>
</div>
<div class="stat-icon bg-gradient-to-br from-orange-500 to-orange-600">
<i class="fas fa-tachometer-alt"></i>
</div>
</div>
</div>
<div class="stat-card">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-semibold text-gray-600 mb-1">平均TPM</p>
<p class="text-3xl font-bold text-rose-600">{{ dashboardData.systemTPM || 0 }}</p>
<p class="text-xs text-gray-500 mt-1">每分钟Token数</p>
</div>
<div class="stat-icon bg-gradient-to-br from-rose-500 to-rose-600">
<i class="fas fa-rocket"></i>
</div>
</div>
</div>
</div>
<!-- 模型消费统计 -->
<div class="mb-8">
<div class="flex items-center justify-between mb-6">
<h3 class="text-xl font-bold text-gray-900">模型使用分布与Token使用趋势</h3>
<div class="flex gap-2 items-center">
<!-- 快捷日期选择 -->
<div class="flex gap-1 bg-gray-100 rounded-lg p-1">
<button
v-for="option in dateFilter.presetOptions"
:key="option.value"
@click="setDateFilterPreset(option.value)"
:class="[
'px-3 py-1 rounded-md text-sm font-medium transition-colors',
dateFilter.preset === option.value && dateFilter.type === 'preset'
? 'bg-white text-blue-600 shadow-sm'
: 'text-gray-600 hover:text-gray-900'
]"
>
{{ option.label }}
</button>
</div>
<!-- 粒度切换按钮 -->
<div class="flex gap-1 bg-gray-100 rounded-lg p-1">
<button
@click="setTrendGranularity('day')"
:class="[
'px-3 py-1 rounded-md text-sm font-medium transition-colors',
trendGranularity === 'day'
? 'bg-white text-blue-600 shadow-sm'
: 'text-gray-600 hover:text-gray-900'
]"
>
<i class="fas fa-calendar-day mr-1"></i>按天
</button>
<button
@click="setTrendGranularity('hour')"
:class="[
'px-3 py-1 rounded-md text-sm font-medium transition-colors',
trendGranularity === 'hour'
? 'bg-white text-blue-600 shadow-sm'
: 'text-gray-600 hover:text-gray-900'
]"
>
<i class="fas fa-clock mr-1"></i>按小时
</button>
</div>
<!-- Element Plus 日期范围选择器 -->
<div class="flex items-center gap-2">
<el-date-picker
:default-time="defaultTime"
v-model="dateFilter.customRange"
type="datetimerange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
@change="onCustomDateRangeChange"
:disabled-date="disabledDate"
size="default"
style="width: 350px;"
class="custom-date-picker"
></el-date-picker>
<span v-if="trendGranularity === 'hour'" class="text-xs text-orange-600">
<i class="fas fa-info-circle"></i> 最多24小时
</span>
</div>
<button @click="refreshChartsData()" class="btn btn-primary px-4 py-2 flex items-center gap-2">
<i class="fas fa-sync-alt"></i>刷新
</button>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- 饼图 -->
<div class="card p-6">
<h4 class="text-lg font-semibold text-gray-800 mb-4">Token使用分布</h4>
<div class="relative" style="height: 300px;">
<canvas id="modelUsageChart"></canvas>
</div>
</div>
<!-- 详细数据表格 -->
<div class="card p-6">
<h4 class="text-lg font-semibold text-gray-800 mb-4">详细统计数据</h4>
<div v-if="dashboardModelStats.length === 0" class="text-center py-8">
<p class="text-gray-500">暂无模型使用数据</p>
</div>
<div v-else class="overflow-auto max-h-[300px]">
<table class="min-w-full">
<thead class="bg-gray-50 sticky top-0">
<tr>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-700">模型</th>
<th class="px-4 py-2 text-right text-xs font-medium text-gray-700">请求数</th>
<th class="px-4 py-2 text-right text-xs font-medium text-gray-700">总Token</th>
<th class="px-4 py-2 text-right text-xs font-medium text-gray-700">费用</th>
<th class="px-4 py-2 text-right text-xs font-medium text-gray-700">占比</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
<tr v-for="stat in dashboardModelStats" :key="stat.model" class="hover:bg-gray-50">
<td class="px-4 py-2 text-sm text-gray-900">{{ stat.model }}</td>
<td class="px-4 py-2 text-sm text-gray-600 text-right">{{ formatNumber(stat.requests) }}</td>
<td class="px-4 py-2 text-sm text-gray-600 text-right">{{ formatNumber(stat.allTokens) }}</td>
<td class="px-4 py-2 text-sm text-green-600 text-right font-medium">{{ stat.formatted ? stat.formatted.total : '$0.000000' }}</td>
<td class="px-4 py-2 text-sm font-medium text-right">
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
{{ calculatePercentage(stat.allTokens, dashboardModelStats) }}%
</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Token使用趋势图 -->
<div class="mb-8">
<div class="card p-6">
<div style="height: 300px;">
<canvas id="usageTrendChart"></canvas>
</div>
</div>
</div>
<!-- API Keys 使用趋势图 -->
<div class="mb-8">
<div class="card p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-900">API Keys 使用趋势</h3>
<!-- 维度切换按钮 -->
<div class="flex gap-1 bg-gray-100 rounded-lg p-1">
<button
@click="apiKeysTrendMetric = 'requests'; updateApiKeysUsageTrendChart()"
:class="[
'px-3 py-1 rounded-md text-sm font-medium transition-colors',
apiKeysTrendMetric === 'requests'
? 'bg-white text-blue-600 shadow-sm'
: 'text-gray-600 hover:text-gray-900'
]"
>
<i class="fas fa-exchange-alt mr-1"></i>请求次数
</button>
<button
@click="apiKeysTrendMetric = 'tokens'; updateApiKeysUsageTrendChart()"
:class="[
'px-3 py-1 rounded-md text-sm font-medium transition-colors',
apiKeysTrendMetric === 'tokens'
? 'bg-white text-blue-600 shadow-sm'
: 'text-gray-600 hover:text-gray-900'
]"
>
<i class="fas fa-coins mr-1"></i>Token 数量
</button>
</div>
</div>
<div class="mb-4 text-sm text-gray-600">
<span v-if="apiKeysTrendData.totalApiKeys > 10">
共 {{ apiKeysTrendData.totalApiKeys }} 个 API Key显示使用量前 10 个
</span>
<span v-else>
共 {{ apiKeysTrendData.totalApiKeys }} 个 API Key
</span>
</div>
<div style="height: 350px;">
<canvas id="apiKeysUsageTrendChart"></canvas>
</div>
</div>
</div>
</div>
<!-- API Keys 管理 -->
<div v-if="activeTab === 'apiKeys'" class="tab-content">
<div class="card p-6">
<div class="flex flex-col md:flex-row justify-between items-center gap-4 mb-6">
<div>
<h3 class="text-xl font-bold text-gray-900 mb-2">API Keys 管理</h3>
<p class="text-gray-600">管理和监控您的 API 密钥</p>
</div>
<button
@click.stop="openCreateApiKeyModal"
class="btn btn-primary px-6 py-3 flex items-center gap-2"
>
<i class="fas fa-plus"></i>创建新 Key
</button>
</div>
<div v-if="apiKeysLoading" class="text-center py-12">
<div class="loading-spinner mx-auto mb-4"></div>
<p class="text-gray-500">正在加载 API Keys...</p>
</div>
<div v-else-if="apiKeys.length === 0" class="text-center py-12">
<div class="w-16 h-16 mx-auto mb-4 bg-gray-100 rounded-full flex items-center justify-center">
<i class="fas fa-key text-gray-400 text-xl"></i>
</div>
<p class="text-gray-500 text-lg">暂无 API Keys</p>
<p class="text-gray-400 text-sm mt-2">点击上方按钮创建您的第一个 API Key</p>
</div>
<div v-else class="table-container">
<table class="min-w-full">
<thead class="bg-gray-50/80 backdrop-blur-sm">
<tr>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">名称</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">API Key</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">状态</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">使用统计</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">创建时间</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">操作</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200/50">
<template v-for="key in apiKeys" :key="key.id">
<!-- API Key 主行 -->
<tr class="table-row">
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="w-8 h-8 bg-gradient-to-br from-blue-500 to-blue-600 rounded-lg flex items-center justify-center mr-3">
<i class="fas fa-key text-white text-xs"></i>
</div>
<div>
<div class="text-sm font-semibold text-gray-900">{{ key.name }}</div>
<div class="text-xs text-gray-500">{{ key.id }}</div>
<div class="text-xs text-gray-500 mt-1">
<span v-if="key.claudeAccountId">
<i class="fas fa-link mr-1"></i>
绑定: {{ getBoundAccountName(key.claudeAccountId) }}
</span>
<span v-else>
<i class="fas fa-share-alt mr-1"></i>
使用共享池
</span>
</div>
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm font-mono text-gray-600 bg-gray-50 px-3 py-1 rounded-lg">
{{ (key.apiKey || '').substring(0, 20) }}...
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span :class="['inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold',
key.isActive ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800']">
<div :class="['w-2 h-2 rounded-full mr-2',
key.isActive ? 'bg-green-500' : 'bg-red-500']"></div>
{{ key.isActive ? '活跃' : '禁用' }}
</span>
</td>
<td class="px-6 py-4">
<div class="space-y-1">
<!-- 请求统计 -->
<div class="flex justify-between text-sm">
<span class="text-gray-600">请求数:</span>
<span class="font-medium text-gray-900">{{ formatNumber((key.usage && key.usage.total && key.usage.total.requests) || 0) }}</span>
</div>
<!-- Token统计 -->
<div class="flex justify-between text-sm">
<span class="text-gray-600">Token:</span>
<span class="font-medium text-gray-900">{{ formatNumber((key.usage && key.usage.total && key.usage.total.tokens) || 0) }}</span>
</div>
<!-- 费用统计 -->
<div class="flex justify-between text-sm">
<span class="text-gray-600">费用:</span>
<span class="font-medium text-green-600">{{ calculateApiKeyCost(key.usage) }}</span>
</div>
<!-- 并发限制 -->
<div class="flex justify-between text-sm">
<span class="text-gray-600">并发限制:</span>
<span class="font-medium text-purple-600">{{ key.concurrencyLimit > 0 ? key.concurrencyLimit : '无限制' }}</span>
</div>
<!-- 当前并发数 -->
<div class="flex justify-between text-sm">
<span class="text-gray-600">当前并发:</span>
<span :class="['font-medium', key.currentConcurrency > 0 ? 'text-orange-600' : 'text-gray-600']">
{{ key.currentConcurrency || 0 }}
<span v-if="key.concurrencyLimit > 0" class="text-xs text-gray-500">/ {{ key.concurrencyLimit }}</span>
</span>
</div>
<!-- 时间窗口限流 -->
<div v-if="key.rateLimitWindow > 0" class="flex justify-between text-sm">
<span class="text-gray-600">时间窗口:</span>
<span class="font-medium text-indigo-600">{{ key.rateLimitWindow }} 分钟</span>
</div>
<!-- 请求次数限制 -->
<div v-if="key.rateLimitRequests > 0" class="flex justify-between text-sm">
<span class="text-gray-600">请求限制:</span>
<span class="font-medium text-indigo-600">{{ key.rateLimitRequests }} 次/窗口</span>
</div>
<!-- 输入/输出Token -->
<div class="flex justify-between text-xs text-gray-500">
<span>输入: {{ formatNumber((key.usage && key.usage.total && key.usage.total.inputTokens) || 0) }}</span>
<span>输出: {{ formatNumber((key.usage && key.usage.total && key.usage.total.outputTokens) || 0) }}</span>
</div>
<!-- RPM/TPM -->
<div class="flex justify-between text-xs text-blue-600">
<span>RPM: {{ (key.usage && key.usage.averages && key.usage.averages.rpm) || 0 }}</span>
<span>TPM: {{ (key.usage && key.usage.averages && key.usage.averages.tpm) || 0 }}</span>
</div>
<!-- 今日统计 -->
<div class="pt-1 border-t border-gray-100">
<div class="flex justify-between text-xs text-green-600">
<span>今日: {{ formatNumber((key.usage && key.usage.daily && key.usage.daily.requests) || 0) }}次</span>
<span>{{ formatNumber((key.usage && key.usage.daily && key.usage.daily.tokens) || 0) }}T</span>
</div>
</div>
<!-- 模型分布按钮 -->
<div class="pt-2">
<button @click="toggleApiKeyModelStats(key.id)" v-if="key && key.id" class="text-xs text-indigo-600 hover:text-indigo-800 font-medium">
<i :class="['fas', expandedApiKeys[key.id] ? 'fa-chevron-up' : 'fa-chevron-down', 'mr-1']"></i>
模型使用分布
</button>
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ new Date(key.createdAt).toLocaleDateString() }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm">
<div class="flex gap-2">
<button
@click="openEditApiKeyModal(key)"
class="text-blue-600 hover:text-blue-900 font-medium hover:bg-blue-50 px-3 py-1 rounded-lg transition-colors"
>
<i class="fas fa-edit mr-1"></i>编辑
</button>
<button
@click="deleteApiKey(key.id)"
class="text-red-600 hover:text-red-900 font-medium hover:bg-red-50 px-3 py-1 rounded-lg transition-colors"
>
<i class="fas fa-trash mr-1"></i>删除
</button>
</div>
</td>
</tr>
<!-- 模型统计展开区域 -->
<tr v-if="key && key.id && expandedApiKeys[key.id]">
<td colspan="6" class="px-6 py-4 bg-gray-50">
<div v-if="!apiKeyModelStats[key.id]" class="text-center py-4">
<div class="loading-spinner mx-auto"></div>
<p class="text-sm text-gray-500 mt-2">加载模型统计...</p>
</div>
<div class="space-y-4">
<!-- 通用的标题和时间筛选器,无论是否有数据都显示 -->
<div class="flex items-center justify-between mb-4">
<h5 class="text-sm font-semibold text-gray-700 flex items-center">
<i class="fas fa-chart-pie text-indigo-500 mr-2"></i>
模型使用分布
</h5>
<div class="flex items-center gap-2">
<span v-if="apiKeyModelStats[key.id] && apiKeyModelStats[key.id].length > 0" class="text-xs text-gray-500 bg-gray-100 px-2 py-1 rounded-full">
{{ apiKeyModelStats[key.id].length }} 个模型
</span>
<!-- API Keys日期筛选器 -->
<div class="flex gap-1 items-center">
<!-- 快捷日期选择 -->
<div class="flex gap-1 bg-gray-100 rounded p-1">
<button
v-for="option in getApiKeyDateFilter(key.id).presetOptions"
:key="option.value"
@click="setApiKeyDateFilterPreset(option.value, key.id)"
:class="[
'px-2 py-1 rounded text-xs font-medium transition-colors',
getApiKeyDateFilter(key.id).preset === option.value && getApiKeyDateFilter(key.id).type === 'preset'
? 'bg-white text-blue-600 shadow-sm'
: 'text-gray-600 hover:text-gray-900'
]"
>
{{ option.label }}
</button>
</div>
<!-- Element Plus 日期范围选择器 -->
<el-date-picker
:model-value="getApiKeyDateFilter(key.id).customRange"
@update:model-value="(value) => onApiKeyCustomDateRangeChange(key.id)(value)"
type="datetimerange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
:disabled-date="disabledDate"
size="small"
style="width: 280px;"
class="api-key-date-picker"
:clearable="true"
:unlink-panels="false"
@visible-change="(visible) => !visible && $forceUpdate()"
></el-date-picker>
</div>
</div>
</div>
<!-- 数据展示区域 -->
<div v-if="apiKeyModelStats[key.id] && apiKeyModelStats[key.id].length === 0" class="text-center py-8">
<div class="flex items-center justify-center gap-2 mb-3">
<i class="fas fa-chart-line text-gray-400 text-lg"></i>
<p class="text-sm text-gray-500">暂无模型使用数据</p>
<button
@click="resetApiKeyDateFilter(key.id)"
class="text-blue-500 hover:text-blue-700 text-sm ml-2 flex items-center gap-1 transition-colors"
title="重置筛选条件并刷新"
>
<i class="fas fa-sync-alt text-xs"></i>
<span class="text-xs">刷新</span>
</button>
</div>
<p class="text-xs text-gray-400">尝试调整时间范围或点击刷新重新加载数据</p>
</div>
<div v-else-if="apiKeyModelStats[key.id] && apiKeyModelStats[key.id].length > 0" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div v-for="stat in apiKeyModelStats[key.id]" :key="stat.model"
class="bg-gradient-to-br from-white to-gray-50 rounded-xl p-4 border border-gray-200 hover:border-indigo-300 hover:shadow-lg transition-all duration-200">
<div class="flex justify-between items-start mb-3">
<div class="flex-1">
<span class="text-sm font-semibold text-gray-800 block mb-1">{{ stat.model }}</span>
<span class="text-xs text-gray-500 bg-blue-50 px-2 py-1 rounded-full">{{ stat.requests }} 次请求</span>
</div>
</div>
<div class="space-y-2 mb-3">
<div class="flex justify-between items-center text-sm">
<span class="text-gray-600 flex items-center">
<i class="fas fa-coins text-yellow-500 mr-1 text-xs"></i>
总Token:
</span>
<span class="font-semibold text-gray-900">{{ formatNumber(stat.allTokens) }}</span>
</div>
<div class="flex justify-between items-center text-sm">
<span class="text-gray-600 flex items-center">
<i class="fas fa-dollar-sign text-green-500 mr-1 text-xs"></i>
费用:
</span>
<span class="font-semibold text-green-600">{{ calculateModelCost(stat) }}</span>
</div>
<div class="flex justify-between items-center text-xs text-gray-500">
<span class="flex items-center">
<i class="fas fa-arrow-down text-green-500 mr-1"></i>
输入:
</span>
<span class="font-medium">{{ formatNumber(stat.inputTokens) }}</span>
</div>
<div class="flex justify-between items-center text-xs text-gray-500">
<span class="flex items-center">
<i class="fas fa-arrow-up text-blue-500 mr-1"></i>
输出:
</span>
<span class="font-medium">{{ formatNumber(stat.outputTokens) }}</span>
</div>
<div v-if="stat.cacheCreateTokens > 0 || stat.cacheReadTokens > 0" class="pt-1 border-t border-gray-100">
<div v-if="stat.cacheCreateTokens > 0" class="flex justify-between items-center text-xs text-purple-600">
<span class="flex items-center">
<i class="fas fa-save mr-1"></i>
缓存创建:
</span>
<span class="font-medium">{{ formatNumber(stat.cacheCreateTokens) }}</span>
</div>
<div v-if="stat.cacheReadTokens > 0" class="flex justify-between items-center text-xs text-purple-600">
<span class="flex items-center">
<i class="fas fa-download mr-1"></i>
缓存读取:
</span>
<span class="font-medium">{{ formatNumber(stat.cacheReadTokens) }}</span>
</div>
</div>
</div>
<!-- 进度条 -->
<div class="w-full bg-gray-200 rounded-full h-2 mt-3">
<div class="bg-gradient-to-r from-indigo-500 to-purple-600 h-2 rounded-full transition-all duration-500"
:style="{ width: calculateApiKeyModelPercentage(stat.allTokens, apiKeyModelStats[key.id]) + '%' }">
</div>
</div>
<div class="text-right mt-1">
<span class="text-xs font-medium text-indigo-600">
{{ calculateApiKeyModelPercentage(stat.allTokens, apiKeyModelStats[key.id]) }}%
</span>
</div>
</div>
</div>
<!-- 总计统计,仅在有数据时显示 -->
<div v-if="apiKeyModelStats[key.id] && apiKeyModelStats[key.id].length > 0" class="mt-4 p-3 bg-gradient-to-r from-indigo-50 to-purple-50 rounded-lg border border-indigo-100">
<div class="flex items-center justify-between text-sm">
<span class="font-semibold text-gray-700 flex items-center">
<i class="fas fa-calculator text-indigo-500 mr-2"></i>
总计统计
</span>
<div class="flex gap-4 text-xs">
<span class="text-gray-600">
总请求: <span class="font-semibold text-gray-800">{{ apiKeyModelStats[key.id].reduce((sum, stat) => sum + stat.requests, 0) }}</span>
</span>
<span class="text-gray-600">
总Token: <span class="font-semibold text-gray-800">{{ formatNumber(apiKeyModelStats[key.id].reduce((sum, stat) => sum + stat.allTokens, 0)) }}</span>
</span>
</div>
</div>
</div>
</div>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
</div>
<!-- Claude 账户管理 -->
<div v-if="activeTab === 'accounts'" class="tab-content">
<div class="card p-6">
<div class="flex flex-col md:flex-row justify-between items-center gap-4 mb-6">
<div>
<h3 class="text-xl font-bold text-gray-900 mb-2">账户管理</h3>
<p class="text-gray-600">管理您的 Claude 和 Gemini 账户及代理配置</p>
</div>
<button
@click.stop="openCreateAccountModal"
class="btn btn-success px-6 py-3 flex items-center gap-2"
>
<i class="fas fa-plus"></i>添加账户
</button>
</div>
<div v-if="accountsLoading" class="text-center py-12">
<div class="loading-spinner mx-auto mb-4"></div>
<p class="text-gray-500">正在加载 Claude 账户...</p>
</div>
<div v-else-if="accounts.length === 0" class="text-center py-12">
<div class="w-16 h-16 mx-auto mb-4 bg-gray-100 rounded-full flex items-center justify-center">
<i class="fas fa-user-circle text-gray-400 text-xl"></i>
</div>
<p class="text-gray-500 text-lg">暂无 Claude 账户</p>
<p class="text-gray-400 text-sm mt-2">点击上方按钮添加您的第一个账户</p>
</div>
<div v-else class="table-container">
<table class="min-w-full">
<thead class="bg-gray-50/80 backdrop-blur-sm">
<tr>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">名称</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">平台</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">类型</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">状态</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">代理</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">最后使用</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">操作</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200/50">
<tr v-for="account in accounts" :key="account.id" class="table-row">
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="w-8 h-8 bg-gradient-to-br from-green-500 to-green-600 rounded-lg flex items-center justify-center mr-3">
<i class="fas fa-user-circle text-white text-xs"></i>
</div>
<div>
<div class="flex items-center gap-2">
<div class="text-sm font-semibold text-gray-900">{{ account.name }}</div>
<span v-if="account.accountType === 'dedicated'"
class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800">
<i class="fas fa-lock mr-1"></i>专属
</span>
<span v-else
class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
<i class="fas fa-share-alt mr-1"></i>共享
</span>
</div>
<div class="text-xs text-gray-500">{{ account.id }}</div>
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span v-if="account.platform === 'gemini'"
class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-yellow-100 text-yellow-800">
<i class="fas fa-robot mr-1"></i>Gemini
</span>
<span v-else
class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-indigo-100 text-indigo-800">
<i class="fas fa-brain mr-1"></i>Claude
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span v-if="account.scopes && account.scopes.length > 0"
class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-blue-100 text-blue-800">
<i class="fas fa-lock mr-1"></i>OAuth
</span>
<span v-else
class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-orange-100 text-orange-800">
<i class="fas fa-key mr-1"></i>传统
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex flex-col gap-1">
<span :class="['inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold',
account.isActive ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800']">
<div :class="['w-2 h-2 rounded-full mr-2',
account.isActive ? 'bg-green-500' : 'bg-red-500']"></div>
{{ account.isActive ? '正常' : '异常' }}
</span>
<span v-if="account.rateLimitStatus && account.rateLimitStatus.isRateLimited"
class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-yellow-100 text-yellow-800">
<i class="fas fa-exclamation-triangle mr-1"></i>
限流中 ({{ account.rateLimitStatus.minutesRemaining }}分钟)
</span>
<span v-if="account.accountType === 'dedicated'"
class="text-xs text-gray-500">
绑定: {{ account.boundApiKeysCount || 0 }} 个API Key
</span>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-600">
<div v-if="account.proxy" class="text-xs bg-blue-50 px-2 py-1 rounded">
{{ account.proxy.type }}://{{ account.proxy.host }}:{{ account.proxy.port }}
</div>
<div v-else class="text-gray-400">无代理</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ account.lastUsedAt ? new Date(account.lastUsedAt).toLocaleDateString() : '从未使用' }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm space-x-2">
<button
@click="openEditAccountModal(account)"
class="text-blue-600 hover:text-blue-900 font-medium hover:bg-blue-50 px-3 py-1 rounded-lg transition-colors"
>
<i class="fas fa-edit mr-1"></i>编辑
</button>
<button
@click="deleteAccount(account.id)"
class="text-red-600 hover:text-red-900 font-medium hover:bg-red-50 px-3 py-1 rounded-lg transition-colors"
>
<i class="fas fa-trash mr-1"></i>删除
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- 模型统计 -->
<div v-if="activeTab === 'models'" class="tab-content">
<div class="card p-6">
<div class="flex flex-col md:flex-row justify-between items-center gap-4 mb-6">
<div>
<h3 class="text-xl font-bold text-gray-900 mb-2">模型使用统计</h3>
<p class="text-gray-600">查看不同模型的使用量和费用统计</p>
</div>
<div class="flex gap-2">
<select v-model="modelStatsPeriod" @change="loadModelStats()" class="form-input px-4 py-2">
<option value="daily">今日</option>
<option value="monthly">本月</option>
</select>
<button @click="loadModelStats()" class="btn btn-primary px-4 py-2 flex items-center gap-2">
<i class="fas fa-sync-alt"></i>刷新
</button>
</div>
</div>
<div v-if="modelStatsLoading" class="text-center py-12">
<div class="loading-spinner mx-auto mb-4"></div>
<p class="text-gray-500">正在加载模型统计...</p>
</div>
<div v-else-if="modelStats.length === 0" class="text-center py-12">
<div class="w-16 h-16 mx-auto mb-4 bg-gray-100 rounded-full flex items-center justify-center">
<i class="fas fa-chart-pie text-gray-400 text-xl"></i>
</div>
<p class="text-gray-500 text-lg">暂无模型使用数据</p>
</div>
<div v-else class="space-y-4">
<div v-for="stat in modelStats" :key="stat.model" class="card p-6 bg-gradient-to-r from-blue-50 to-purple-50">
<div class="flex justify-between items-start mb-4">
<div>
<h4 class="text-lg font-bold text-gray-900 mb-1">{{ stat.model }}</h4>
<p class="text-sm text-gray-600">{{ stat.period === 'daily' ? '今日使用' : '本月使用' }}</p>
</div>
<div class="text-right">
<div class="text-2xl font-bold text-green-600">{{ (stat.formatted && stat.formatted.total) || '$0.000000' }}</div>
<div class="text-sm text-gray-500">总费用</div>
</div>
</div>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-4">
<div class="text-center p-3 bg-white rounded-lg">
<div class="text-lg font-semibold text-blue-600">{{ formatNumber((stat.usage && stat.usage.requests) || 0) }}</div>
<div class="text-xs text-gray-500">请求数</div>
</div>
<div class="text-center p-3 bg-white rounded-lg">
<div class="text-lg font-semibold text-green-600">{{ formatNumber((stat.usage && stat.usage.inputTokens) || 0) }}</div>
<div class="text-xs text-gray-500">输入Token</div>
</div>
<div class="text-center p-3 bg-white rounded-lg">
<div class="text-lg font-semibold text-orange-600">{{ formatNumber((stat.usage && stat.usage.outputTokens) || 0) }}</div>
<div class="text-xs text-gray-500">输出Token</div>
</div>
<div class="text-center p-3 bg-white rounded-lg">
<div class="text-lg font-semibold text-purple-600">{{ formatNumber((stat.usage && stat.usage.totalTokens) || 0) }}</div>
<div class="text-xs text-gray-500">总Token</div>
</div>
</div>
<!-- Cache Token统计 -->
<div v-if="((stat.usage && stat.usage.cacheCreateTokens) || 0) > 0 || ((stat.usage && stat.usage.cacheReadTokens) || 0) > 0" class="grid grid-cols-2 gap-4 mb-4">
<div class="text-center p-3 bg-blue-50 rounded-lg border border-blue-200">
<div class="text-lg font-semibold text-blue-600">{{ formatNumber((stat.usage && stat.usage.cacheCreateTokens) || 0) }}</div>
<div class="text-xs text-blue-500">缓存创建</div>
</div>
<div class="text-center p-3 bg-blue-50 rounded-lg border border-blue-200">
<div class="text-lg font-semibold text-blue-600">{{ formatNumber((stat.usage && stat.usage.cacheReadTokens) || 0) }}</div>
<div class="text-xs text-blue-500">缓存读取</div>
</div>
</div>
<!-- 费用明细 -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<div class="text-center p-3 bg-green-50 rounded-lg border border-green-200">
<div class="text-sm font-semibold text-green-600">{{ (stat.formatted && stat.formatted.input) || '$0.000000' }}</div>
<div class="text-xs text-green-500">输入费用</div>
</div>
<div class="text-center p-3 bg-orange-50 rounded-lg border border-orange-200">
<div class="text-sm font-semibold text-orange-600">{{ (stat.formatted && stat.formatted.output) || '$0.000000' }}</div>
<div class="text-xs text-orange-500">输出费用</div>
</div>
<div class="text-center p-3 bg-blue-50 rounded-lg border border-blue-200">
<div class="text-sm font-semibold text-blue-600">{{ (stat.formatted && stat.formatted.cacheWrite) || '$0.000000' }}</div>
<div class="text-xs text-blue-500">缓存写入</div>
</div>
<div class="text-center p-3 bg-purple-50 rounded-lg border border-purple-200">
<div class="text-sm font-semibold text-purple-600">{{ (stat.formatted && stat.formatted.cacheRead) || '$0.000000' }}</div>
<div class="text-xs text-purple-500">缓存读取</div>
</div>
</div>
<!-- 定价信息 -->
<div class="mt-4 p-3 bg-gray-50 rounded-lg">
<div class="text-xs text-gray-600 mb-2">定价信息 (USD per 1M tokens):</div>
<div class="grid grid-cols-4 gap-2 text-xs">
<div>输入: ${{ (stat.pricing && stat.pricing.input) || 0 }}</div>
<div>输出: ${{ (stat.pricing && stat.pricing.output) || 0 }}</div>
<div>缓存写: ${{ (stat.pricing && stat.pricing.cacheWrite) || 0 }}</div>
<div>缓存读: ${{ (stat.pricing && stat.pricing.cacheRead) || 0 }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div v-if="activeTab === 'tutorial'" class="tab-content">
<div class="card p-6">
<div class="mb-8">
<h3 class="text-2xl font-bold text-gray-900 mb-4 flex items-center">
<i class="fas fa-graduation-cap text-blue-600 mr-3"></i>
Claude Code 使用教程
</h3>
<p class="text-gray-600 text-lg">跟着这个教程,你可以轻松在自己的电脑上安装并使用 Claude Code。</p>
</div>
<!-- 系统选择标签 -->
<div class="mb-8">
<div class="flex flex-wrap gap-2 p-2 bg-gray-100 rounded-xl">
<button
v-for="system in tutorialSystems"
:key="system.key"
@click="activeTutorialSystem = system.key"
:class="['flex-1 py-3 px-6 text-sm font-semibold rounded-lg transition-all duration-300 flex items-center justify-center gap-2',
activeTutorialSystem === system.key
? 'bg-white text-blue-600 shadow-sm'
: 'text-gray-600 hover:bg-white/50 hover:text-gray-900']"
>
<i :class="system.icon"></i>
{{ system.name }}
</button>
</div>
</div>
<!-- Windows 教程 -->
<div v-if="activeTutorialSystem === 'windows'" class="tutorial-content">
<!-- 第一步:安装 Node.js -->
<div class="mb-10">
<h4 class="text-xl font-semibold text-gray-800 mb-4 flex items-center">
<span class="w-8 h-8 bg-blue-500 text-white rounded-full flex items-center justify-center text-sm font-bold mr-3">1</span>
安装 Node.js 环境
</h4>
<p class="text-gray-600 mb-6">Claude Code 需要 Node.js 环境才能运行。</p>
<div class="bg-gradient-to-r from-blue-50 to-indigo-50 rounded-xl p-6 border border-blue-100 mb-6">
<h5 class="text-lg font-semibold text-gray-800 mb-3 flex items-center">
<i class="fab fa-windows text-blue-600 mr-2"></i>
Windows 安装方法
</h5>
<div class="mb-4">
<p class="text-gray-700 mb-3">方法一:官网下载(推荐)</p>
<ol class="list-decimal list-inside text-gray-600 space-y-2 ml-4">
<li>打开浏览器访问 <code class="bg-gray-100 px-2 py-1 rounded text-sm">https://nodejs.org/</code></li>
<li>点击 "LTS" 版本进行下载(推荐长期支持版本)</li>
<li>下载完成后双击 <code class="bg-gray-100 px-2 py-1 rounded text-sm">.msi</code> 文件</li>
<li>按照安装向导完成安装,保持默认设置即可</li>
</ol>
</div>
<div class="mb-4">
<p class="text-gray-700 mb-3">方法二:使用包管理器</p>
<p class="text-gray-600 mb-2">如果你安装了 Chocolatey 或 Scoop可以使用命令行安装</p>
<div class="bg-gray-900 text-green-400 p-4 rounded-lg font-mono text-sm">
<div class="mb-2"># 使用 Chocolatey</div>
<div class="text-gray-300">choco install nodejs</div>
<div class="mt-3 mb-2"># 或使用 Scoop</div>
<div class="text-gray-300">scoop install nodejs</div>
</div>
</div>
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
<h6 class="font-medium text-blue-800 mb-2">Windows 注意事项</h6>
<ul class="text-blue-700 text-sm space-y-1">
<li>• 建议使用 PowerShell 而不是 CMD</li>
<li>• 如果遇到权限问题,尝试以管理员身份运行</li>
<li>• 某些杀毒软件可能会误报,需要添加白名单</li>
</ul>
</div>
</div>
<!-- 验证安装 -->
<div class="bg-green-50 border border-green-200 rounded-lg p-4">
<h6 class="font-medium text-green-800 mb-2">验证安装是否成功</h6>
<p class="text-green-700 text-sm mb-3">安装完成后,打开 PowerShell 或 CMD输入以下命令</p>
<div class="bg-gray-900 text-green-400 p-3 rounded font-mono text-sm">
<div class="text-gray-300">node --version</div>
<div class="text-gray-300">npm --version</div>
</div>
<p class="text-green-700 text-sm mt-2">如果显示版本号,说明安装成功了!</p>
</div>
</div>
<!-- 第二步:安装 Git Bash -->
<div class="mb-10">
<h4 class="text-xl font-semibold text-gray-800 mb-4 flex items-center">
<span class="w-8 h-8 bg-green-500 text-white rounded-full flex items-center justify-center text-sm font-bold mr-3">2</span>
安装 Git Bash
</h4>
<p class="text-gray-600 mb-6">Windows 环境下需要使用 Git Bash 安装Claude code。安装完成后环境变量设置和使用 Claude Code 仍然在普通的 PowerShell 或 CMD 中进行。</p>
<div class="bg-gradient-to-r from-green-50 to-emerald-50 rounded-xl p-6 border border-green-100 mb-6">
<h5 class="text-lg font-semibold text-gray-800 mb-3 flex items-center">
<i class="fab fa-git-alt text-green-600 mr-2"></i>
下载并安装 Git for Windows
</h5>
<ol class="list-decimal list-inside text-gray-600 space-y-2 ml-4 mb-4">
<li>访问 <code class="bg-gray-100 px-2 py-1 rounded text-sm">https://git-scm.com/downloads/win</code></li>
<li>点击 "Download for Windows" 下载安装包</li>
<li>运行下载的 <code class="bg-gray-100 px-2 py-1 rounded text-sm">.exe</code> 安装文件</li>
<li>在安装过程中保持默认设置,直接点击 "Next" 完成安装</li>
</ol>
<div class="bg-green-50 border border-green-200 rounded-lg p-4">
<h6 class="font-medium text-green-800 mb-2">安装完成后</h6>
<ul class="text-green-700 text-sm space-y-1">
<li>• 在任意文件夹右键可以看到 "Git Bash Here" 选项</li>
<li>• 也可以从开始菜单启动 "Git Bash"</li>
<li>• 只需要在 Git Bash 中运行 npm install 命令</li>
<li>• 后续的环境变量设置和使用都在 PowerShell/CMD 中</li>
</ul>
</div>
</div>
<!-- 验证安装 -->
<div class="bg-green-50 border border-green-200 rounded-lg p-4">
<h6 class="font-medium text-green-800 mb-2">验证 Git Bash 安装</h6>
<p class="text-green-700 text-sm mb-3">打开 Git Bash输入以下命令验证</p>
<div class="bg-gray-900 text-green-400 p-3 rounded font-mono text-sm">
<div class="text-gray-300">git --version</div>
</div>
<p class="text-green-700 text-sm mt-2">如果显示 Git 版本号,说明安装成功!</p>
</div>
</div>
<!-- 第三步:安装 Claude Code -->
<div class="mb-10">
<h4 class="text-xl font-semibold text-gray-800 mb-4 flex items-center">
<span class="w-8 h-8 bg-purple-500 text-white rounded-full flex items-center justify-center text-sm font-bold mr-3">3</span>
安装 Claude Code
</h4>
<div class="bg-gradient-to-r from-purple-50 to-pink-50 rounded-xl p-6 border border-purple-100 mb-6">
<h5 class="text-lg font-semibold text-gray-800 mb-3 flex items-center">
<i class="fas fa-download text-purple-600 mr-2"></i>
安装 Claude Code
</h5>
<p class="text-gray-700 mb-4">打开 Git Bash重要不要使用 PowerShell运行以下命令</p>
<div class="bg-gray-900 text-green-400 p-4 rounded-lg font-mono text-sm mb-4">
<div class="mb-2"># 在 Git Bash 中全局安装 Claude Code</div>
<div class="text-gray-300">npm install -g @anthropic-ai/claude-code</div>
</div>
<p class="text-gray-600 text-sm">这个命令会从 npm 官方仓库下载并安装最新版本的 Claude Code。</p>
<div class="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mt-4">
<h6 class="font-medium text-yellow-800 mb-2">重要提醒</h6>
<ul class="text-yellow-700 text-sm space-y-1">
<li>• 必须在 Git Bash 中运行,不要在 PowerShell 中运行</li>
<li>• 如果遇到权限问题,可以尝试在 Git Bash 中使用 sudo 命令</li>
</ul>
</div>
</div>
<!-- 验证安装 -->
<div class="bg-green-50 border border-green-200 rounded-lg p-4">
<h6 class="font-medium text-green-800 mb-2">验证 Claude Code 安装</h6>
<p class="text-green-700 text-sm mb-3">安装完成后,输入以下命令检查是否安装成功:</p>
<div class="bg-gray-900 text-green-400 p-3 rounded font-mono text-sm">
<div class="text-gray-300">claude --version</div>
</div>
<p class="text-green-700 text-sm mt-2">如果显示版本号恭喜你Claude Code 已经成功安装了。</p>
</div>
</div>
<!-- 第四步:设置环境变量 -->
<div class="mb-10">
<h4 class="text-xl font-semibold text-gray-800 mb-4 flex items-center">
<span class="w-8 h-8 bg-orange-500 text-white rounded-full flex items-center justify-center text-sm font-bold mr-3">4</span>
设置环境变量
</h4>
<div class="bg-gradient-to-r from-orange-50 to-yellow-50 rounded-xl p-6 border border-orange-100 mb-6">
<h5 class="text-lg font-semibold text-gray-800 mb-3 flex items-center">
<i class="fas fa-cog text-orange-600 mr-2"></i>
配置 Claude Code 环境变量
</h5>
<p class="text-gray-700 mb-4">为了让 Claude Code 连接到你的中转服务,需要设置两个环境变量:</p>
<div class="space-y-4">
<div class="bg-white rounded-lg p-4 border border-orange-200">
<h6 class="font-medium text-gray-800 mb-2">方法一PowerShell 临时设置(推荐)</h6>
<p class="text-gray-600 text-sm mb-3">在 PowerShell 中运行以下命令:</p>
<div class="bg-gray-900 text-green-400 p-3 rounded font-mono text-sm">
<div class="text-gray-300">$env:ANTHROPIC_BASE_URL = "{{ currentBaseUrl }}"</div>
<div class="text-gray-300">$env:ANTHROPIC_AUTH_TOKEN = "你的API密钥"</div>
</div>
<p class="text-yellow-700 text-xs mt-2">💡 记得将 "你的API密钥" 替换为在上方 "API Keys" 标签页中创建的实际密钥。</p>
</div>
<div class="bg-white rounded-lg p-4 border border-orange-200">
<h6 class="font-medium text-gray-800 mb-2">方法二:系统环境变量(永久设置)</h6>
<ol class="text-gray-600 text-sm space-y-1 list-decimal list-inside">
<li>右键"此电脑" → "属性" → "高级系统设置"</li>
<li>点击"环境变量"按钮</li>
<li>在"用户变量"或"系统变量"中点击"新建"</li>
<li>添加以下两个变量:</li>
</ol>
<div class="mt-3 space-y-2">
<div class="bg-gray-100 p-2 rounded text-sm">
<strong>变量名:</strong> ANTHROPIC_BASE_URL<br>
<strong>变量值:</strong> <span class="font-mono">{{ currentBaseUrl }}</span>
</div>
<div class="bg-gray-100 p-2 rounded text-sm">
<strong>变量名:</strong> ANTHROPIC_AUTH_TOKEN<br>
<strong>变量值:</strong> <span class="font-mono">你的API密钥</span>
</div>
</div>
</div>
</div>
</div>
<!-- 验证环境变量设置 -->
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4 mt-6">
<h6 class="font-medium text-blue-800 mb-2">验证环境变量设置</h6>
<p class="text-blue-700 text-sm mb-3">设置完环境变量后,可以通过以下命令验证是否设置成功:</p>
<div class="space-y-4">
<div>
<h6 class="font-medium text-gray-800 mb-2">在 PowerShell 中验证:</h6>
<div class="bg-gray-900 text-green-400 p-3 rounded font-mono text-sm space-y-1">
<div class="text-gray-300">echo $env:ANTHROPIC_BASE_URL</div>
<div class="text-gray-300">echo $env:ANTHROPIC_AUTH_TOKEN</div>
</div>
</div>
<div>
<h6 class="font-medium text-gray-800 mb-2">在 CMD 中验证:</h6>
<div class="bg-gray-900 text-green-400 p-3 rounded font-mono text-sm space-y-1">
<div class="text-gray-300">echo %ANTHROPIC_BASE_URL%</div>
<div class="text-gray-300">echo %ANTHROPIC_AUTH_TOKEN%</div>
</div>
</div>
</div>
<div class="mt-3 space-y-2">
<p class="text-blue-700 text-sm">
<strong>预期输出示例:</strong>
</p>
<div class="bg-gray-100 p-2 rounded text-sm font-mono">
<div>{{ currentBaseUrl }}</div>
<div>cr_xxxxxxxxxxxxxxxxxx</div>
</div>
<p class="text-blue-700 text-xs">
💡 如果输出为空或显示变量名本身,说明环境变量设置失败,请重新设置。
</p>
</div>
</div>
</div>
<!-- 第五步:开始使用 -->
<div class="mb-8">
<h4 class="text-xl font-semibold text-gray-800 mb-4 flex items-center">
<span class="w-8 h-8 bg-yellow-500 text-white rounded-full flex items-center justify-center text-sm font-bold mr-3">5</span>
开始使用 Claude Code
</h4>
<div class="bg-gradient-to-r from-yellow-50 to-amber-50 rounded-xl p-6 border border-yellow-100">
<p class="text-gray-700 mb-4">现在你可以开始使用 Claude Code 了!</p>
<div class="space-y-4">
<div>
<h6 class="font-medium text-gray-800 mb-2">启动 Claude Code</h6>
<div class="bg-gray-900 text-green-400 p-3 rounded font-mono text-sm">
<div class="text-gray-300">claude</div>
</div>
</div>
<div>
<h6 class="font-medium text-gray-800 mb-2">在特定项目中使用</h6>
<div class="bg-gray-900 text-green-400 p-3 rounded font-mono text-sm">
<div class="mb-2"># 进入你的项目目录</div>
<div class="text-gray-300">cd C:\path\to\your\project</div>
<div class="mt-2 mb-2"># 启动 Claude Code</div>
<div class="text-gray-300">claude</div>
</div>
</div>
</div>
</div>
</div>
<!-- Windows 故障排除 -->
<div class="mb-8">
<h4 class="text-xl font-semibold text-gray-800 mb-4 flex items-center">
<i class="fas fa-wrench text-red-600 mr-3"></i>
Windows 常见问题解决
</h4>
<div class="space-y-4">
<details class="bg-gray-50 rounded-lg border border-gray-200">
<summary class="p-4 cursor-pointer font-medium text-gray-800 hover:bg-gray-100">
安装时提示 "permission denied" 错误
</summary>
<div class="px-4 pb-4 text-gray-600">
<p class="mb-2">这通常是权限问题,尝试以下解决方法:</p>
<ul class="list-disc list-inside space-y-1 text-sm">
<li>以管理员身份运行 PowerShell</li>
<li>或者配置 npm 使用用户目录:<code class="bg-gray-200 px-1 rounded">npm config set prefix %APPDATA%\npm</code></li>
</ul>
</div>
</details>
<details class="bg-gray-50 rounded-lg border border-gray-200">
<summary class="p-4 cursor-pointer font-medium text-gray-800 hover:bg-gray-100">
PowerShell 执行策略错误
</summary>
<div class="px-4 pb-4 text-gray-600">
<p class="mb-2">如果遇到执行策略限制,运行:</p>
<div class="bg-gray-900 text-green-400 p-3 rounded font-mono text-sm">
<div class="text-gray-300">Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser</div>
</div>
</div>
</details>
<details class="bg-gray-50 rounded-lg border border-gray-200">
<summary class="p-4 cursor-pointer font-medium text-gray-800 hover:bg-gray-100">
环境变量设置后不生效
</summary>
<div class="px-4 pb-4 text-gray-600">
<p class="mb-2">设置永久环境变量后需要:</p>
<ul class="list-disc list-inside space-y-1 text-sm">
<li>重新启动 PowerShell 或 CMD</li>
<li>或者注销并重新登录 Windows</li>
<li>验证设置:<code class="bg-gray-200 px-1 rounded">echo $env:ANTHROPIC_BASE_URL</code></li>
</ul>
</div>
</details>
</div>
</div>
</div>
<!-- macOS 教程 -->
<div v-if="activeTutorialSystem === 'macos'" class="tutorial-content">
<!-- 第一步:安装 Node.js -->
<div class="mb-10">
<h4 class="text-xl font-semibold text-gray-800 mb-4 flex items-center">
<span class="w-8 h-8 bg-blue-500 text-white rounded-full flex items-center justify-center text-sm font-bold mr-3">1</span>
安装 Node.js 环境
</h4>
<p class="text-gray-600 mb-6">Claude Code 需要 Node.js 环境才能运行。</p>
<div class="bg-gradient-to-r from-gray-50 to-slate-50 rounded-xl p-6 border border-gray-200 mb-6">
<h5 class="text-lg font-semibold text-gray-800 mb-3 flex items-center">
<i class="fab fa-apple text-gray-700 mr-2"></i>
macOS 安装方法
</h5>
<div class="mb-4">
<p class="text-gray-700 mb-3">方法一:使用 Homebrew推荐</p>
<p class="text-gray-600 mb-2">如果你已经安装了 Homebrew使用它安装 Node.js 会更方便:</p>
<div class="bg-gray-900 text-green-400 p-4 rounded-lg font-mono text-sm">
<div class="mb-2"># 更新 Homebrew</div>
<div class="text-gray-300">brew update</div>
<div class="mt-3 mb-2"># 安装 Node.js</div>
<div class="text-gray-300">brew install node</div>
</div>
</div>
<div class="mb-4">
<p class="text-gray-700 mb-3">方法二:官网下载</p>
<ol class="list-decimal list-inside text-gray-600 space-y-2 ml-4">
<li>访问 <code class="bg-gray-100 px-2 py-1 rounded text-sm">https://nodejs.org/</code></li>
<li>下载适合 macOS 的 LTS 版本</li>
<li>打开下载的 <code class="bg-gray-100 px-2 py-1 rounded text-sm">.pkg</code> 文件</li>
<li>按照安装程序指引完成安装</li>
</ol>
</div>
<div class="bg-gray-50 border border-gray-200 rounded-lg p-4">
<h6 class="font-medium text-gray-800 mb-2">macOS 注意事项</h6>
<ul class="text-gray-700 text-sm space-y-1">
<li>• 如果遇到权限问题,可能需要使用 <code class="bg-gray-200 px-1 rounded">sudo</code></li>
<li>• 首次运行可能需要在系统偏好设置中允许</li>
<li>• 建议使用 Terminal 或 iTerm2</li>
</ul>
</div>
</div>
<!-- 验证安装 -->
<div class="bg-green-50 border border-green-200 rounded-lg p-4">
<h6 class="font-medium text-green-800 mb-2">验证安装是否成功</h6>
<p class="text-green-700 text-sm mb-3">安装完成后,打开 Terminal输入以下命令</p>
<div class="bg-gray-900 text-green-400 p-3 rounded font-mono text-sm">
<div class="text-gray-300">node --version</div>
<div class="text-gray-300">npm --version</div>
</div>
<p class="text-green-700 text-sm mt-2">如果显示版本号,说明安装成功了!</p>
</div>
</div>
<!-- 第二步:安装 Claude Code -->
<div class="mb-10">
<h4 class="text-xl font-semibold text-gray-800 mb-4 flex items-center">
<span class="w-8 h-8 bg-green-500 text-white rounded-full flex items-center justify-center text-sm font-bold mr-3">2</span>
安装 Claude Code
</h4>
<div class="bg-gradient-to-r from-purple-50 to-pink-50 rounded-xl p-6 border border-purple-100 mb-6">
<h5 class="text-lg font-semibold text-gray-800 mb-3 flex items-center">
<i class="fas fa-download text-purple-600 mr-2"></i>
安装 Claude Code
</h5>
<p class="text-gray-700 mb-4">打开 Terminal运行以下命令</p>
<div class="bg-gray-900 text-green-400 p-4 rounded-lg font-mono text-sm mb-4">
<div class="mb-2"># 全局安装 Claude Code</div>
<div class="text-gray-300">npm install -g @anthropic-ai/claude-code</div>
</div>
<p class="text-gray-600 text-sm mb-2">如果遇到权限问题,可以使用 sudo</p>
<div class="bg-gray-900 text-green-400 p-4 rounded-lg font-mono text-sm">
<div class="text-gray-300">sudo npm install -g @anthropic-ai/claude-code</div>
</div>
</div>
<!-- 验证安装 -->
<div class="bg-green-50 border border-green-200 rounded-lg p-4">
<h6 class="font-medium text-green-800 mb-2">验证 Claude Code 安装</h6>
<p class="text-green-700 text-sm mb-3">安装完成后,输入以下命令检查是否安装成功:</p>
<div class="bg-gray-900 text-green-400 p-3 rounded font-mono text-sm">
<div class="text-gray-300">claude --version</div>
</div>
<p class="text-green-700 text-sm mt-2">如果显示版本号恭喜你Claude Code 已经成功安装了。</p>
</div>
</div>
<!-- 第三步:设置环境变量 -->
<div class="mb-10">
<h4 class="text-xl font-semibold text-gray-800 mb-4 flex items-center">
<span class="w-8 h-8 bg-orange-500 text-white rounded-full flex items-center justify-center text-sm font-bold mr-3">3</span>
设置环境变量
</h4>
<div class="bg-gradient-to-r from-orange-50 to-yellow-50 rounded-xl p-6 border border-orange-100 mb-6">
<h5 class="text-lg font-semibold text-gray-800 mb-3 flex items-center">
<i class="fas fa-cog text-orange-600 mr-2"></i>
配置 Claude Code 环境变量
</h5>
<p class="text-gray-700 mb-4">为了让 Claude Code 连接到你的中转服务,需要设置两个环境变量:</p>
<div class="space-y-4">
<div class="bg-white rounded-lg p-4 border border-orange-200">
<h6 class="font-medium text-gray-800 mb-2">方法一:临时设置(当前会话)</h6>
<p class="text-gray-600 text-sm mb-3">在 Terminal 中运行以下命令:</p>
<div class="bg-gray-900 text-green-400 p-3 rounded font-mono text-sm">
<div class="text-gray-300">export ANTHROPIC_BASE_URL="{{ currentBaseUrl }}"</div>
<div class="text-gray-300">export ANTHROPIC_AUTH_TOKEN="你的API密钥"</div>
</div>
<p class="text-yellow-700 text-xs mt-2">💡 记得将 "你的API密钥" 替换为在上方 "API Keys" 标签页中创建的实际密钥。</p>
</div>
<div class="bg-white rounded-lg p-4 border border-orange-200">
<h6 class="font-medium text-gray-800 mb-2">方法二:永久设置</h6>
<p class="text-gray-600 text-sm mb-3">编辑你的 shell 配置文件(根据你使用的 shell</p>
<div class="bg-gray-900 text-green-400 p-3 rounded font-mono text-sm mb-3">
<div class="mb-2"># 对于 zsh (默认)</div>
<div class="text-gray-300">echo 'export ANTHROPIC_BASE_URL="{{ currentBaseUrl }}"' >> ~/.zshrc</div>
<div class="text-gray-300">echo 'export ANTHROPIC_AUTH_TOKEN="你的API密钥"' >> ~/.zshrc</div>
<div class="text-gray-300">source ~/.zshrc</div>
</div>
<div class="bg-gray-900 text-green-400 p-3 rounded font-mono text-sm">
<div class="mb-2"># 对于 bash</div>
<div class="text-gray-300">echo 'export ANTHROPIC_BASE_URL="{{ currentBaseUrl }}"' >> ~/.bash_profile</div>
<div class="text-gray-300">echo 'export ANTHROPIC_AUTH_TOKEN="你的API密钥"' >> ~/.bash_profile</div>
<div class="text-gray-300">source ~/.bash_profile</div>
</div>
</div>
</div>
</div>
</div>
<!-- 第四步:开始使用 -->
<div class="mb-8">
<h4 class="text-xl font-semibold text-gray-800 mb-4 flex items-center">
<span class="w-8 h-8 bg-yellow-500 text-white rounded-full flex items-center justify-center text-sm font-bold mr-3">4</span>
开始使用 Claude Code
</h4>
<div class="bg-gradient-to-r from-yellow-50 to-amber-50 rounded-xl p-6 border border-yellow-100">
<p class="text-gray-700 mb-4">现在你可以开始使用 Claude Code 了!</p>
<div class="space-y-4">
<div>
<h6 class="font-medium text-gray-800 mb-2">启动 Claude Code</h6>
<div class="bg-gray-900 text-green-400 p-3 rounded font-mono text-sm">
<div class="text-gray-300">claude</div>
</div>
</div>
<div>
<h6 class="font-medium text-gray-800 mb-2">在特定项目中使用</h6>
<div class="bg-gray-900 text-green-400 p-3 rounded font-mono text-sm">
<div class="mb-2"># 进入你的项目目录</div>
<div class="text-gray-300">cd /path/to/your/project</div>
<div class="mt-2 mb-2"># 启动 Claude Code</div>
<div class="text-gray-300">claude</div>
</div>
</div>
</div>
</div>
</div>
<!-- macOS 故障排除 -->
<div class="mb-8">
<h4 class="text-xl font-semibold text-gray-800 mb-4 flex items-center">
<i class="fas fa-wrench text-red-600 mr-3"></i>
macOS 常见问题解决
</h4>
<div class="space-y-4">
<details class="bg-gray-50 rounded-lg border border-gray-200">
<summary class="p-4 cursor-pointer font-medium text-gray-800 hover:bg-gray-100">
安装时提示权限错误
</summary>
<div class="px-4 pb-4 text-gray-600">
<p class="mb-2">尝试以下解决方法:</p>
<ul class="list-disc list-inside space-y-1 text-sm">
<li>使用 sudo 安装:<code class="bg-gray-200 px-1 rounded">sudo npm install -g @anthropic-ai/claude-code</code></li>
<li>或者配置 npm 使用用户目录:<code class="bg-gray-200 px-1 rounded">npm config set prefix ~/.npm-global</code></li>
</ul>
</div>
</details>
<details class="bg-gray-50 rounded-lg border border-gray-200">
<summary class="p-4 cursor-pointer font-medium text-gray-800 hover:bg-gray-100">
macOS 安全设置阻止运行
</summary>
<div class="px-4 pb-4 text-gray-600">
<p class="mb-2">如果系统阻止运行 Claude Code</p>
<ul class="list-disc list-inside space-y-1 text-sm">
<li>打开"系统偏好设置" → "安全性与隐私"</li>
<li>点击"仍要打开"或"允许"</li>
<li>或者在 Terminal 中运行:<code class="bg-gray-200 px-1 rounded">sudo spctl --master-disable</code></li>
</ul>
</div>
</details>
<details class="bg-gray-50 rounded-lg border border-gray-200">
<summary class="p-4 cursor-pointer font-medium text-gray-800 hover:bg-gray-100">
环境变量不生效
</summary>
<div class="px-4 pb-4 text-gray-600">
<p class="mb-2">检查以下几点:</p>
<ul class="list-disc list-inside space-y-1 text-sm">
<li>确认修改了正确的配置文件(.zshrc 或 .bash_profile</li>
<li>重新启动 Terminal</li>
<li>验证设置:<code class="bg-gray-200 px-1 rounded">echo $ANTHROPIC_BASE_URL</code></li>
</ul>
</div>
</details>
</div>
</div>
</div>
<!-- Linux 教程 -->
<div v-if="activeTutorialSystem === 'linux'" class="tutorial-content">
<!-- 第一步:安装 Node.js -->
<div class="mb-10">
<h4 class="text-xl font-semibold text-gray-800 mb-4 flex items-center">
<span class="w-8 h-8 bg-blue-500 text-white rounded-full flex items-center justify-center text-sm font-bold mr-3">1</span>
安装 Node.js 环境
</h4>
<p class="text-gray-600 mb-6">Claude Code 需要 Node.js 环境才能运行。</p>
<div class="bg-gradient-to-r from-orange-50 to-red-50 rounded-xl p-6 border border-orange-100 mb-6">
<h5 class="text-lg font-semibold text-gray-800 mb-3 flex items-center">
<i class="fab fa-ubuntu text-orange-600 mr-2"></i>
Linux 安装方法
</h5>
<div class="mb-4">
<p class="text-gray-700 mb-3">方法一:使用官方仓库(推荐)</p>
<div class="bg-gray-900 text-green-400 p-4 rounded-lg font-mono text-sm">
<div class="mb-2"># 添加 NodeSource 仓库</div>
<div class="text-gray-300">curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -</div>
<div class="mt-3 mb-2"># 安装 Node.js</div>
<div class="text-gray-300">sudo apt-get install -y nodejs</div>
</div>
</div>
<div class="mb-4">
<p class="text-gray-700 mb-3">方法二:使用系统包管理器</p>
<p class="text-gray-600 mb-2">虽然版本可能不是最新的,但对于基本使用已经足够:</p>
<div class="bg-gray-900 text-green-400 p-4 rounded-lg font-mono text-sm">
<div class="mb-2"># Ubuntu/Debian</div>
<div class="text-gray-300">sudo apt update</div>
<div class="text-gray-300">sudo apt install nodejs npm</div>
<div class="mt-3 mb-2"># CentOS/RHEL/Fedora</div>
<div class="text-gray-300">sudo dnf install nodejs npm</div>
</div>
</div>
<div class="bg-orange-50 border border-orange-200 rounded-lg p-4">
<h6 class="font-medium text-orange-800 mb-2">Linux 注意事项</h6>
<ul class="text-orange-700 text-sm space-y-1">
<li>• 某些发行版可能需要安装额外的依赖</li>
<li>• 如果遇到权限问题,使用 <code class="bg-orange-200 px-1 rounded">sudo</code></li>
<li>• 确保你的用户在 npm 的全局目录有写权限</li>
</ul>
</div>
</div>
<!-- 验证安装 -->
<div class="bg-green-50 border border-green-200 rounded-lg p-4">
<h6 class="font-medium text-green-800 mb-2">验证安装是否成功</h6>
<p class="text-green-700 text-sm mb-3">安装完成后,打开终端,输入以下命令:</p>
<div class="bg-gray-900 text-green-400 p-3 rounded font-mono text-sm">
<div class="text-gray-300">node --version</div>
<div class="text-gray-300">npm --version</div>
</div>
<p class="text-green-700 text-sm mt-2">如果显示版本号,说明安装成功了!</p>
</div>
</div>
<!-- 第二步:安装 Claude Code -->
<div class="mb-10">
<h4 class="text-xl font-semibold text-gray-800 mb-4 flex items-center">
<span class="w-8 h-8 bg-green-500 text-white rounded-full flex items-center justify-center text-sm font-bold mr-3">2</span>
安装 Claude Code
</h4>
<div class="bg-gradient-to-r from-purple-50 to-pink-50 rounded-xl p-6 border border-purple-100 mb-6">
<h5 class="text-lg font-semibold text-gray-800 mb-3 flex items-center">
<i class="fas fa-download text-purple-600 mr-2"></i>
安装 Claude Code
</h5>
<p class="text-gray-700 mb-4">打开终端,运行以下命令:</p>
<div class="bg-gray-900 text-green-400 p-4 rounded-lg font-mono text-sm mb-4">
<div class="mb-2"># 全局安装 Claude Code</div>
<div class="text-gray-300">npm install -g @anthropic-ai/claude-code</div>
</div>
<p class="text-gray-600 text-sm mb-2">如果遇到权限问题,可以使用 sudo</p>
<div class="bg-gray-900 text-green-400 p-4 rounded-lg font-mono text-sm">
<div class="text-gray-300">sudo npm install -g @anthropic-ai/claude-code</div>
</div>
</div>
<!-- 验证安装 -->
<div class="bg-green-50 border border-green-200 rounded-lg p-4">
<h6 class="font-medium text-green-800 mb-2">验证 Claude Code 安装</h6>
<p class="text-green-700 text-sm mb-3">安装完成后,输入以下命令检查是否安装成功:</p>
<div class="bg-gray-900 text-green-400 p-3 rounded font-mono text-sm">
<div class="text-gray-300">claude --version</div>
</div>
<p class="text-green-700 text-sm mt-2">如果显示版本号恭喜你Claude Code 已经成功安装了。</p>
</div>
</div>
<!-- 第三步:设置环境变量 -->
<div class="mb-10">
<h4 class="text-xl font-semibold text-gray-800 mb-4 flex items-center">
<span class="w-8 h-8 bg-orange-500 text-white rounded-full flex items-center justify-center text-sm font-bold mr-3">3</span>
设置环境变量
</h4>
<div class="bg-gradient-to-r from-orange-50 to-yellow-50 rounded-xl p-6 border border-orange-100 mb-6">
<h5 class="text-lg font-semibold text-gray-800 mb-3 flex items-center">
<i class="fas fa-cog text-orange-600 mr-2"></i>
配置 Claude Code 环境变量
</h5>
<p class="text-gray-700 mb-4">为了让 Claude Code 连接到你的中转服务,需要设置两个环境变量:</p>
<div class="space-y-4">
<div class="bg-white rounded-lg p-4 border border-orange-200">
<h6 class="font-medium text-gray-800 mb-2">方法一:临时设置(当前会话)</h6>
<p class="text-gray-600 text-sm mb-3">在终端中运行以下命令:</p>
<div class="bg-gray-900 text-green-400 p-3 rounded font-mono text-sm">
<div class="text-gray-300">export ANTHROPIC_BASE_URL="{{ currentBaseUrl }}"</div>
<div class="text-gray-300">export ANTHROPIC_AUTH_TOKEN="你的API密钥"</div>
</div>
<p class="text-yellow-700 text-xs mt-2">💡 记得将 "你的API密钥" 替换为在上方 "API Keys" 标签页中创建的实际密钥。</p>
</div>
<div class="bg-white rounded-lg p-4 border border-orange-200">
<h6 class="font-medium text-gray-800 mb-2">方法二:永久设置</h6>
<p class="text-gray-600 text-sm mb-3">编辑你的 shell 配置文件:</p>
<div class="bg-gray-900 text-green-400 p-3 rounded font-mono text-sm mb-3">
<div class="mb-2"># 对于 bash (默认)</div>
<div class="text-gray-300">echo 'export ANTHROPIC_BASE_URL="{{ currentBaseUrl }}"' >> ~/.bashrc</div>
<div class="text-gray-300">echo 'export ANTHROPIC_AUTH_TOKEN="你的API密钥"' >> ~/.bashrc</div>
<div class="text-gray-300">source ~/.bashrc</div>
</div>
<div class="bg-gray-900 text-green-400 p-3 rounded font-mono text-sm">
<div class="mb-2"># 对于 zsh</div>
<div class="text-gray-300">echo 'export ANTHROPIC_BASE_URL="{{ currentBaseUrl }}"' >> ~/.zshrc</div>
<div class="text-gray-300">echo 'export ANTHROPIC_AUTH_TOKEN="你的API密钥"' >> ~/.zshrc</div>
<div class="text-gray-300">source ~/.zshrc</div>
</div>
</div>
</div>
</div>
</div>
<!-- 第四步:开始使用 -->
<div class="mb-8">
<h4 class="text-xl font-semibold text-gray-800 mb-4 flex items-center">
<span class="w-8 h-8 bg-yellow-500 text-white rounded-full flex items-center justify-center text-sm font-bold mr-3">4</span>
开始使用 Claude Code
</h4>
<div class="bg-gradient-to-r from-yellow-50 to-amber-50 rounded-xl p-6 border border-yellow-100">
<p class="text-gray-700 mb-4">现在你可以开始使用 Claude Code 了!</p>
<div class="space-y-4">
<div>
<h6 class="font-medium text-gray-800 mb-2">启动 Claude Code</h6>
<div class="bg-gray-900 text-green-400 p-3 rounded font-mono text-sm">
<div class="text-gray-300">claude</div>
</div>
</div>
<div>
<h6 class="font-medium text-gray-800 mb-2">在特定项目中使用</h6>
<div class="bg-gray-900 text-green-400 p-3 rounded font-mono text-sm">
<div class="mb-2"># 进入你的项目目录</div>
<div class="text-gray-300">cd /path/to/your/project</div>
<div class="mt-2 mb-2"># 启动 Claude Code</div>
<div class="text-gray-300">claude</div>
</div>
</div>
</div>
</div>
</div>
<!-- Linux 故障排除 -->
<div class="mb-8">
<h4 class="text-xl font-semibold text-gray-800 mb-4 flex items-center">
<i class="fas fa-wrench text-red-600 mr-3"></i>
Linux 常见问题解决
</h4>
<div class="space-y-4">
<details class="bg-gray-50 rounded-lg border border-gray-200">
<summary class="p-4 cursor-pointer font-medium text-gray-800 hover:bg-gray-100">
安装时提示权限错误
</summary>
<div class="px-4 pb-4 text-gray-600">
<p class="mb-2">尝试以下解决方法:</p>
<ul class="list-disc list-inside space-y-1 text-sm">
<li>使用 sudo 安装:<code class="bg-gray-200 px-1 rounded">sudo npm install -g @anthropic-ai/claude-code</code></li>
<li>或者配置 npm 使用用户目录:<code class="bg-gray-200 px-1 rounded">npm config set prefix ~/.npm-global</code></li>
<li>然后添加到 PATH<code class="bg-gray-200 px-1 rounded">export PATH=~/.npm-global/bin:$PATH</code></li>
</ul>
</div>
</details>
<details class="bg-gray-50 rounded-lg border border-gray-200">
<summary class="p-4 cursor-pointer font-medium text-gray-800 hover:bg-gray-100">
缺少依赖库
</summary>
<div class="px-4 pb-4 text-gray-600">
<p class="mb-2">某些 Linux 发行版需要安装额外依赖:</p>
<div class="bg-gray-900 text-green-400 p-3 rounded font-mono text-sm">
<div class="mb-2"># Ubuntu/Debian</div>
<div class="text-gray-300">sudo apt install build-essential</div>
<div class="mt-2 mb-2"># CentOS/RHEL</div>
<div class="text-gray-300">sudo dnf groupinstall "Development Tools"</div>
</div>
</div>
</details>
<details class="bg-gray-50 rounded-lg border border-gray-200">
<summary class="p-4 cursor-pointer font-medium text-gray-800 hover:bg-gray-100">
环境变量不生效
</summary>
<div class="px-4 pb-4 text-gray-600">
<p class="mb-2">检查以下几点:</p>
<ul class="list-disc list-inside space-y-1 text-sm">
<li>确认修改了正确的配置文件(.bashrc 或 .zshrc</li>
<li>重新启动终端或运行 <code class="bg-gray-200 px-1 rounded">source ~/.bashrc</code></li>
<li>验证设置:<code class="bg-gray-200 px-1 rounded">echo $ANTHROPIC_BASE_URL</code></li>
</ul>
</div>
</details>
</div>
</div>
</div>
<!-- 结尾 -->
<div class="bg-gradient-to-r from-blue-500 to-purple-600 text-white rounded-xl p-6 text-center">
<h5 class="text-xl font-semibold mb-2">🎉 恭喜你!</h5>
<p class="text-blue-100 mb-4">你已经成功安装并配置了 Claude Code现在可以开始享受 AI 编程助手带来的便利了。</p>
<p class="text-sm text-blue-200">如果在使用过程中遇到任何问题,可以查看官方文档或社区讨论获取帮助。</p>
</div>
</div>
</div>
</div>
</div>
<!-- 创建 API Key 模态框 -->
<div v-if="showCreateApiKeyModal" class="fixed inset-0 modal z-50 flex items-center justify-center p-4">
<div class="modal-content w-full max-w-md p-8 mx-auto max-h-[90vh] flex flex-col">
<div class="flex items-center justify-between mb-6">
<div class="flex items-center gap-3">
<div class="w-10 h-10 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center">
<i class="fas fa-key text-white"></i>
</div>
<h3 class="text-xl font-bold text-gray-900">创建新的 API Key</h3>
</div>
<button
@click="showCreateApiKeyModal = false"
class="text-gray-400 hover:text-gray-600 transition-colors"
>
<i class="fas fa-times text-xl"></i>
</button>
</div>
<form @submit.prevent="createApiKey" class="space-y-6 modal-scroll-content custom-scrollbar flex-1">
<div>
<label class="block text-sm font-semibold text-gray-700 mb-3">名称</label>
<input
v-model="apiKeyForm.name"
type="text"
required
class="form-input w-full"
placeholder="为您的 API Key 取一个名称"
>
</div>
<!-- 速率限制设置 -->
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4 space-y-4">
<div class="flex items-start gap-3 mb-3">
<div class="w-8 h-8 bg-blue-500 rounded-lg flex items-center justify-center flex-shrink-0">
<i class="fas fa-tachometer-alt text-white text-sm"></i>
</div>
<div class="flex-1">
<h4 class="font-semibold text-gray-800 mb-1">速率限制设置 (可选)</h4>
<p class="text-sm text-gray-600">控制 API Key 的使用频率和资源消耗</p>
</div>
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-3">时间窗口 (分钟)</label>
<input
v-model="apiKeyForm.rateLimitWindow"
type="number"
min="1"
placeholder="留空表示无限制"
class="form-input w-full"
>
<p class="text-xs text-gray-500 mt-2">设置一个时间段(以分钟为单位),用于计算速率限制</p>
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-3">时间窗口内的请求次数限制</label>
<input
v-model="apiKeyForm.rateLimitRequests"
type="number"
min="1"
placeholder="留空表示无限制"
class="form-input w-full"
>
<p class="text-xs text-gray-500 mt-2">在时间窗口内允许的最大请求次数(需要先设置时间窗口)</p>
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-3">时间窗口内的 Token 使用量限制</label>
<input
v-model="apiKeyForm.tokenLimit"
type="number"
placeholder="留空表示无限制"
class="form-input w-full"
>
<p class="text-xs text-gray-500 mt-2">在时间窗口内允许消耗的最大 Token 数量(需要先设置时间窗口)</p>
</div>
<!-- 示例说明 -->
<div class="bg-blue-100 rounded-lg p-3 mt-3">
<h5 class="text-sm font-semibold text-blue-800 mb-2">💡 使用示例</h5>
<div class="text-xs text-blue-700 space-y-1">
<p><strong>示例1:</strong> 时间窗口=60请求次数限制=100</p>
<p class="ml-4">→ 每60分钟内最多允许100次请求</p>
<p class="mt-2"><strong>示例2:</strong> 时间窗口=10Token限制=50000</p>
<p class="ml-4">→ 每10分钟内最多消耗50,000个Token</p>
<p class="mt-2"><strong>示例3:</strong> 时间窗口=30请求次数限制=50Token限制=100000</p>
<p class="ml-4">→ 每30分钟内最多50次请求且总Token不超过100,000</p>
</div>
</div>
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-3">并发限制 (可选)</label>
<input
v-model="apiKeyForm.concurrencyLimit"
type="number"
min="0"
placeholder="0 表示无限制"
class="form-input w-full"
>
<p class="text-xs text-gray-500 mt-2">设置此 API Key 可同时处理的最大请求数0 或留空表示无限制</p>
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-3">备注 (可选)</label>
<textarea
v-model="apiKeyForm.description"
rows="3"
class="form-input w-full resize-none"
placeholder="描述此 API Key 的用途..."
></textarea>
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-3">服务权限</label>
<div class="flex gap-4">
<label class="flex items-center cursor-pointer">
<input
type="radio"
v-model="apiKeyForm.permissions"
value="all"
class="mr-2"
>
<span class="text-sm text-gray-700">全部服务</span>
</label>
<label class="flex items-center cursor-pointer">
<input
type="radio"
v-model="apiKeyForm.permissions"
value="claude"
class="mr-2"
>
<span class="text-sm text-gray-700">仅 Claude</span>
</label>
<label class="flex items-center cursor-pointer">
<input
type="radio"
v-model="apiKeyForm.permissions"
value="gemini"
class="mr-2"
>
<span class="text-sm text-gray-700">仅 Gemini</span>
</label>
</div>
<p class="text-xs text-gray-500 mt-2">控制此 API Key 可以访问哪些服务</p>
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-3">专属账号绑定 (可选)</label>
<div class="grid grid-cols-1 gap-3">
<div>
<label class="block text-sm font-medium text-gray-600 mb-1">Claude 专属账号</label>
<select
v-model="apiKeyForm.claudeAccountId"
class="form-input w-full"
:disabled="apiKeyForm.permissions === 'gemini'"
>
<option value="">使用共享账号池</option>
<option
v-for="account in dedicatedAccounts.filter(a => a.platform === 'claude')"
:key="account.id"
:value="account.id"
>
{{ account.name }} ({{ account.status === 'active' ? '正常' : '异常' }})
</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-600 mb-1">Gemini 专属账号</label>
<select
v-model="apiKeyForm.geminiAccountId"
class="form-input w-full"
:disabled="apiKeyForm.permissions === 'claude'"
>
<option value="">使用共享账号池</option>
<option
v-for="account in dedicatedAccounts.filter(a => a.platform === 'gemini')"
:key="account.id"
:value="account.id"
>
{{ account.name }} ({{ account.status === 'active' ? '正常' : '异常' }})
</option>
</select>
</div>
</div>
<p class="text-xs text-gray-500 mt-2">选择专属账号后此API Key将只使用该账号不选择则使用共享账号池</p>
</div>
<div>
<div class="flex items-center mb-3">
<input
type="checkbox"
v-model="apiKeyForm.enableModelRestriction"
id="enableModelRestriction"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
>
<label for="enableModelRestriction" class="ml-2 text-sm font-semibold text-gray-700 cursor-pointer">
启用模型限制
</label>
</div>
<div v-if="apiKeyForm.enableModelRestriction" class="space-y-3">
<div>
<label class="block text-sm font-medium text-gray-600 mb-2">限制的模型列表</label>
<div class="flex flex-wrap gap-2 mb-3 min-h-[32px] p-2 bg-gray-50 rounded-lg border border-gray-200">
<span
v-for="(model, index) in apiKeyForm.restrictedModels"
:key="index"
class="inline-flex items-center px-3 py-1 rounded-full text-sm bg-red-100 text-red-800"
>
{{ model }}
<button
type="button"
@click="removeRestrictedModel(apiKeyForm, index)"
class="ml-2 text-red-600 hover:text-red-800"
>
<i class="fas fa-times text-xs"></i>
</button>
</span>
<span v-if="apiKeyForm.restrictedModels.length === 0" class="text-gray-400 text-sm">
暂无限制的模型
</span>
</div>
<div class="flex gap-2">
<input
v-model="apiKeyForm.modelInput"
@keydown.enter.prevent="addRestrictedModel(apiKeyForm)"
type="text"
placeholder="输入模型名称,按回车添加"
class="form-input flex-1"
>
<button
type="button"
@click="addRestrictedModel(apiKeyForm)"
class="px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors"
>
<i class="fas fa-plus"></i>
</button>
</div>
<p class="text-xs text-gray-500 mt-2">设置此API Key无法访问的模型例如claude-opus-4-20250514</p>
</div>
</div>
</div>
<div class="flex gap-3 pt-4">
<button
type="button"
@click="showCreateApiKeyModal = false"
class="flex-1 px-6 py-3 bg-gray-100 text-gray-700 rounded-xl font-semibold hover:bg-gray-200 transition-colors"
>
取消
</button>
<button
type="submit"
:disabled="createApiKeyLoading"
class="btn btn-primary flex-1 py-3 px-6 font-semibold"
>
<div v-if="createApiKeyLoading" class="loading-spinner mr-2"></div>
<i v-else class="fas fa-plus mr-2"></i>
{{ createApiKeyLoading ? '创建中...' : '创建' }}
</button>
</div>
</form>
</div>
</div>
<!-- 编辑 API Key 模态框 -->
<div v-if="showEditApiKeyModal" class="fixed inset-0 modal z-50 flex items-center justify-center p-4">
<div class="modal-content w-full max-w-md p-8 mx-auto max-h-[90vh] flex flex-col">
<div class="flex items-center justify-between mb-6">
<div class="flex items-center gap-3">
<div class="w-10 h-10 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center">
<i class="fas fa-edit text-white"></i>
</div>
<h3 class="text-xl font-bold text-gray-900">编辑 API Key</h3>
</div>
<button
@click="closeEditApiKeyModal"
class="text-gray-400 hover:text-gray-600 transition-colors"
>
<i class="fas fa-times text-xl"></i>
</button>
</div>
<form @submit.prevent="updateApiKey" class="space-y-6 modal-scroll-content custom-scrollbar flex-1">
<div>
<label class="block text-sm font-semibold text-gray-700 mb-3">名称</label>
<input
:value="editApiKeyForm.name"
type="text"
disabled
class="form-input w-full bg-gray-100 cursor-not-allowed"
>
<p class="text-xs text-gray-500 mt-2">名称不可修改</p>
</div>
<!-- 速率限制设置 -->
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4 space-y-4">
<div class="flex items-start gap-3 mb-3">
<div class="w-8 h-8 bg-blue-500 rounded-lg flex items-center justify-center flex-shrink-0">
<i class="fas fa-tachometer-alt text-white text-sm"></i>
</div>
<div class="flex-1">
<h4 class="font-semibold text-gray-800 mb-1">速率限制设置</h4>
<p class="text-sm text-gray-600">控制 API Key 的使用频率和资源消耗</p>
</div>
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-3">时间窗口 (分钟)</label>
<input
v-model="editApiKeyForm.rateLimitWindow"
type="number"
min="1"
placeholder="留空表示无限制"
class="form-input w-full"
>
<p class="text-xs text-gray-500 mt-2">设置一个时间段(以分钟为单位),用于计算速率限制</p>
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-3">时间窗口内的请求次数限制</label>
<input
v-model="editApiKeyForm.rateLimitRequests"
type="number"
min="1"
placeholder="留空表示无限制"
class="form-input w-full"
>
<p class="text-xs text-gray-500 mt-2">在时间窗口内允许的最大请求次数(需要先设置时间窗口)</p>
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-3">时间窗口内的 Token 使用量限制</label>
<input
v-model="editApiKeyForm.tokenLimit"
type="number"
min="0"
placeholder="0 表示无限制"
class="form-input w-full"
>
<p class="text-xs text-gray-500 mt-2">在时间窗口内允许消耗的最大 Token 数量需要先设置时间窗口0 或留空表示无限制</p>
</div>
<!-- 示例说明 -->
<div class="bg-blue-100 rounded-lg p-3 mt-3">
<h5 class="text-sm font-semibold text-blue-800 mb-2">💡 使用示例</h5>
<div class="text-xs text-blue-700 space-y-1">
<p><strong>示例1:</strong> 时间窗口=60请求次数限制=100</p>
<p class="ml-4">→ 每60分钟内最多允许100次请求</p>
<p class="mt-2"><strong>示例2:</strong> 时间窗口=10Token限制=50000</p>
<p class="ml-4">→ 每10分钟内最多消耗50,000个Token</p>
<p class="mt-2"><strong>示例3:</strong> 时间窗口=30请求次数限制=50Token限制=100000</p>
<p class="ml-4">→ 每30分钟内最多50次请求且总Token不超过100,000</p>
</div>
</div>
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-3">并发限制</label>
<input
v-model="editApiKeyForm.concurrencyLimit"
type="number"
min="0"
placeholder="0 表示无限制"
class="form-input w-full"
>
<p class="text-xs text-gray-500 mt-2">设置此 API Key 可同时处理的最大请求数0 或留空表示无限制</p>
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-3">服务权限</label>
<div class="flex gap-4">
<label class="flex items-center cursor-pointer">
<input
type="radio"
v-model="editApiKeyForm.permissions"
value="all"
class="mr-2"
>
<span class="text-sm text-gray-700">全部服务</span>
</label>
<label class="flex items-center cursor-pointer">
<input
type="radio"
v-model="editApiKeyForm.permissions"
value="claude"
class="mr-2"
>
<span class="text-sm text-gray-700">仅 Claude</span>
</label>
<label class="flex items-center cursor-pointer">
<input
type="radio"
v-model="editApiKeyForm.permissions"
value="gemini"
class="mr-2"
>
<span class="text-sm text-gray-700">仅 Gemini</span>
</label>
</div>
<p class="text-xs text-gray-500 mt-2">控制此 API Key 可以访问哪些服务</p>
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-3">专属账号绑定</label>
<div class="grid grid-cols-1 gap-3">
<div>
<label class="block text-sm font-medium text-gray-600 mb-1">Claude 专属账号</label>
<select
v-model="editApiKeyForm.claudeAccountId"
class="form-input w-full"
:disabled="editApiKeyForm.permissions === 'gemini'"
>
<option value="">使用共享账号池</option>
<option
v-for="account in dedicatedAccounts.filter(a => a.platform === 'claude')"
:key="account.id"
:value="account.id"
>
{{ account.name }} ({{ account.status === 'active' ? '正常' : '异常' }})
</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-600 mb-1">Gemini 专属账号</label>
<select
v-model="editApiKeyForm.geminiAccountId"
class="form-input w-full"
:disabled="editApiKeyForm.permissions === 'claude'"
>
<option value="">使用共享账号池</option>
<option
v-for="account in dedicatedAccounts.filter(a => a.platform === 'gemini')"
:key="account.id"
:value="account.id"
>
{{ account.name }} ({{ account.status === 'active' ? '正常' : '异常' }})
</option>
</select>
</div>
</div>
<p class="text-xs text-gray-500 mt-2">修改绑定账号将影响此API Key的请求路由</p>
</div>
<div>
<div class="flex items-center mb-3">
<input
type="checkbox"
v-model="editApiKeyForm.enableModelRestriction"
id="editEnableModelRestriction"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
>
<label for="editEnableModelRestriction" class="ml-2 text-sm font-semibold text-gray-700 cursor-pointer">
启用模型限制
</label>
</div>
<div v-if="editApiKeyForm.enableModelRestriction" class="space-y-3">
<div>
<label class="block text-sm font-medium text-gray-600 mb-2">限制的模型列表</label>
<div class="flex flex-wrap gap-2 mb-3 min-h-[32px] p-2 bg-gray-50 rounded-lg border border-gray-200">
<span
v-for="(model, index) in editApiKeyForm.restrictedModels"
:key="index"
class="inline-flex items-center px-3 py-1 rounded-full text-sm bg-red-100 text-red-800"
>
{{ model }}
<button
type="button"
@click="removeRestrictedModel(editApiKeyForm, index)"
class="ml-2 text-red-600 hover:text-red-800"
>
<i class="fas fa-times text-xs"></i>
</button>
</span>
<span v-if="editApiKeyForm.restrictedModels.length === 0" class="text-gray-400 text-sm">
暂无限制的模型
</span>
</div>
<div class="flex gap-2">
<input
v-model="editApiKeyForm.modelInput"
@keydown.enter.prevent="addRestrictedModel(editApiKeyForm)"
type="text"
placeholder="输入模型名称,按回车添加"
class="form-input flex-1"
>
<button
type="button"
@click="addRestrictedModel(editApiKeyForm)"
class="px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors"
>
<i class="fas fa-plus"></i>
</button>
</div>
<p class="text-xs text-gray-500 mt-2">设置此API Key无法访问的模型例如claude-opus-4-20250514</p>
</div>
</div>
</div>
<div class="flex gap-3 pt-4">
<button
type="button"
@click="closeEditApiKeyModal"
class="flex-1 px-6 py-3 bg-gray-100 text-gray-700 rounded-xl font-semibold hover:bg-gray-200 transition-colors"
>
取消
</button>
<button
type="submit"
:disabled="editApiKeyLoading"
class="btn btn-primary flex-1 py-3 px-6 font-semibold"
>
<div v-if="editApiKeyLoading" class="loading-spinner mr-2"></div>
<i v-else class="fas fa-save mr-2"></i>
{{ editApiKeyLoading ? '保存中...' : '保存修改' }}
</button>
</div>
</form>
</div>
</div>
<!-- 新创建的 API Key 展示弹窗 -->
<div v-if="showNewApiKeyModal" class="fixed inset-0 modal z-50 flex items-center justify-center p-4">
<div class="modal-content w-full max-w-lg p-8 mx-auto max-h-[90vh] overflow-y-auto custom-scrollbar">
<div class="flex items-center justify-between mb-6">
<div class="flex items-center gap-3">
<div class="w-12 h-12 bg-gradient-to-br from-green-500 to-green-600 rounded-xl flex items-center justify-center">
<i class="fas fa-check text-white text-lg"></i>
</div>
<div>
<h3 class="text-xl font-bold text-gray-900">API Key 创建成功</h3>
<p class="text-sm text-gray-600">请妥善保存您的 API Key</p>
</div>
</div>
<button
@click="closeNewApiKeyModal"
class="text-gray-400 hover:text-gray-600 transition-colors"
>
<i class="fas fa-times text-xl"></i>
</button>
</div>
<!-- 警告提示 -->
<div class="bg-amber-50 border-l-4 border-amber-400 p-4 mb-6">
<div class="flex items-start">
<div class="w-6 h-6 bg-amber-400 rounded-lg flex items-center justify-center flex-shrink-0 mt-0.5">
<i class="fas fa-exclamation-triangle text-white text-sm"></i>
</div>
<div class="ml-3">
<h5 class="font-semibold text-amber-900 mb-1">重要提醒</h5>
<p class="text-sm text-amber-800">
这是您唯一能看到完整 API Key 的机会。关闭此窗口后,系统将不再显示完整的 API Key。请立即复制并妥善保存。
</p>
</div>
</div>
</div>
<!-- API Key 信息 -->
<div class="space-y-4 mb-6">
<div>
<label class="block text-sm font-semibold text-gray-700 mb-2">API Key 名称</label>
<div class="p-3 bg-gray-50 rounded-lg border">
<span class="text-gray-900 font-medium">{{ newApiKey.name }}</span>
</div>
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-2">备注</label>
<div class="p-3 bg-gray-50 rounded-lg border">
<span class="text-gray-700">{{ newApiKey.description }}</span>
</div>
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-2">API Key</label>
<div class="relative">
<div class="p-4 pr-14 bg-gray-900 rounded-lg border font-mono text-sm text-white break-all min-h-[60px] flex items-center">
{{ getDisplayedApiKey() }}
</div>
<div class="absolute top-3 right-3">
<button
@click="toggleApiKeyVisibility"
class="p-2 bg-gray-700 hover:bg-gray-600 rounded-lg transition-colors shadow-lg"
:title="newApiKey.showFullKey ? '隐藏完整Key' : '显示完整Key'"
>
<i :class="newApiKey.showFullKey ? 'fas fa-eye-slash' : 'fas fa-eye'" class="text-white text-sm"></i>
</button>
</div>
</div>
<p class="text-xs text-gray-500 mt-2">
<i class="fas fa-info-circle mr-1"></i>
点击眼睛图标切换显示模式,使用下方按钮复制完整 API Key
</p>
</div>
</div>
<!-- 操作按钮 -->
<div class="flex gap-3">
<button
@click="copyApiKeyToClipboard"
class="flex-1 btn btn-primary py-3 px-6 font-semibold flex items-center justify-center gap-2"
>
<i class="fas fa-copy"></i>
复制 API Key
</button>
<button
@click="closeNewApiKeyModal"
class="px-6 py-3 bg-gray-100 text-gray-700 rounded-xl font-semibold hover:bg-gray-200 transition-colors"
>
我已保存
</button>
</div>
</div>
</div>
<!-- 创建账户模态框 -->
<div v-if="showCreateAccountModal" class="fixed inset-0 modal z-50 flex items-center justify-center p-4">
<div class="modal-content w-full max-w-2xl p-8 mx-auto max-h-[90vh] overflow-y-auto custom-scrollbar">
<div class="flex items-center justify-between mb-6">
<div class="flex items-center gap-3">
<div class="w-10 h-10 bg-gradient-to-br from-green-500 to-green-600 rounded-xl flex items-center justify-center">
<i class="fas fa-user-circle text-white"></i>
</div>
<h3 class="text-xl font-bold text-gray-900">添加账户</h3>
</div>
<button
@click="closeCreateAccountModal"
class="text-gray-400 hover:text-gray-600 transition-colors"
>
<i class="fas fa-times text-xl"></i>
</button>
</div>
<!-- 步骤指示器 -->
<div class="flex items-center justify-center mb-8">
<div class="flex items-center space-x-4">
<div class="flex items-center">
<div :class="['w-8 h-8 rounded-full flex items-center justify-center text-sm font-semibold',
oauthStep >= 1 ? 'bg-blue-500 text-white' : 'bg-gray-200 text-gray-500']">
1
</div>
<span class="ml-2 text-sm font-medium text-gray-700">基本信息</span>
</div>
<div class="w-8 h-0.5 bg-gray-300"></div>
<div class="flex items-center">
<div :class="['w-8 h-8 rounded-full flex items-center justify-center text-sm font-semibold',
oauthStep >= 2 ? 'bg-blue-500 text-white' : 'bg-gray-200 text-gray-500']">
2
</div>
<span class="ml-2 text-sm font-medium text-gray-700">授权认证</span>
</div>
</div>
</div>
<!-- 步骤1: 基本信息和代理设置 -->
<div v-if="oauthStep === 1">
<div class="space-y-6">
<div>
<label class="block text-sm font-semibold text-gray-700 mb-3">平台</label>
<div class="flex gap-4">
<label class="flex items-center cursor-pointer">
<input
type="radio"
v-model="accountForm.platform"
value="claude"
class="mr-2"
>
<span class="text-sm text-gray-700">Claude</span>
</label>
<label class="flex items-center cursor-pointer">
<input
type="radio"
v-model="accountForm.platform"
value="gemini"
class="mr-2"
>
<span class="text-sm text-gray-700">Gemini</span>
</label>
</div>
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-3">添加方式</label>
<div class="flex gap-4">
<label class="flex items-center cursor-pointer">
<input
type="radio"
v-model="accountForm.addType"
value="oauth"
class="mr-2"
>
<span class="text-sm text-gray-700">OAuth 授权 (推荐)</span>
</label>
<label class="flex items-center cursor-pointer">
<input
type="radio"
v-model="accountForm.addType"
value="manual"
class="mr-2"
>
<span class="text-sm text-gray-700">手动输入 Access Token</span>
</label>
</div>
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-3">账户名称</label>
<input
v-model="accountForm.name"
type="text"
required
class="form-input w-full"
placeholder="为账户设置一个易识别的名称"
>
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-3">描述 (可选)</label>
<textarea
v-model="accountForm.description"
rows="3"
class="form-input w-full resize-none"
placeholder="账户用途说明..."
></textarea>
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-3">账户类型</label>
<div class="flex gap-4">
<label class="flex items-center cursor-pointer">
<input
type="radio"
v-model="accountForm.accountType"
value="shared"
class="mr-2"
>
<span class="text-sm text-gray-700">共享账户</span>
</label>
<label class="flex items-center cursor-pointer">
<input
type="radio"
v-model="accountForm.accountType"
value="dedicated"
class="mr-2"
>
<span class="text-sm text-gray-700">专属账户</span>
</label>
</div>
<p class="text-xs text-gray-500 mt-2">
共享账户供所有API Key使用专属账户仅供特定API Key使用
</p>
</div>
<!-- Gemini 项目编号字段 -->
<div v-if="accountForm.platform === 'gemini'">
<label class="block text-sm font-semibold text-gray-700 mb-3">项目编号 (可选)</label>
<input
v-model="accountForm.projectId"
type="text"
class="form-input w-full"
placeholder="例如123456789012纯数字"
>
<div class="mt-2 p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
<div class="flex items-start gap-2">
<i class="fas fa-info-circle text-yellow-600 mt-0.5"></i>
<div class="text-xs text-yellow-700">
<p class="font-medium mb-1">Google Cloud/Workspace 账号需要提供项目编号</p>
<p>某些 Google 账号(特别是绑定了 Google Cloud 的账号)会被识别为 Workspace 账号,需要提供额外的项目编号。</p>
<div class="mt-2 p-2 bg-white rounded border border-yellow-300">
<p class="font-medium mb-1">如何获取项目编号:</p>
<ol class="list-decimal list-inside space-y-1 ml-2">
<li>访问 <a href="https://console.cloud.google.com/welcome" target="_blank" class="text-blue-600 hover:underline font-medium">Google Cloud Console</a></li>
<li>复制<span class="font-semibold text-red-600">项目编号Project Number</span>通常是12位纯数字</li>
<li class="text-red-600">⚠️ 注意不要复制项目IDProject ID要复制项目编号</li>
</ol>
</div>
<p class="mt-2"><strong>提示:</strong>如果您的账号是普通个人账号(未绑定 Google Cloud请留空此字段。</p>
</div>
</div>
</div>
</div>
<!-- 手动输入 Token 字段 -->
<div v-if="accountForm.addType === 'manual'" class="space-y-4 bg-blue-50 p-4 rounded-lg border border-blue-200">
<div class="flex items-start gap-3 mb-4">
<div class="w-8 h-8 bg-blue-500 rounded-lg flex items-center justify-center flex-shrink-0 mt-1">
<i class="fas fa-info text-white text-sm"></i>
</div>
<div>
<h5 class="font-semibold text-blue-900 mb-2">手动输入 Token</h5>
<p v-if="accountForm.platform === 'claude'" class="text-sm text-blue-800 mb-2">
请输入有效的 Claude Access Token。如果您有 Refresh Token建议也一并填写以支持自动刷新。
</p>
<p v-else-if="accountForm.platform === 'gemini'" class="text-sm text-blue-800 mb-2">
请输入有效的 Gemini Access Token。如果您有 Refresh Token建议也一并填写以支持自动刷新。
</p>
<div class="bg-white/80 rounded-lg p-3 mt-2 mb-2 border border-blue-300">
<p class="text-sm text-blue-900 font-medium mb-1">
<i class="fas fa-folder-open mr-1"></i>
获取 Access Token 的方法:
</p>
<p v-if="accountForm.platform === 'claude'" class="text-xs text-blue-800">
请从已登录 Claude Code 的机器上获取 <code class="bg-blue-100 px-1 py-0.5 rounded font-mono">~/.claude/.credentials.json</code> 文件中的凭证,
请勿使用 Claude 官网 API Keys 页面的密钥。
</p>
<p v-else-if="accountForm.platform === 'gemini'" class="text-xs text-blue-800">
请从已登录 Gemini CLI 的机器上获取 <code class="bg-blue-100 px-1 py-0.5 rounded font-mono">~/.config/gemini/credentials.json</code> 文件中的凭证。
</p>
</div>
<p class="text-xs text-blue-600">💡 如果未填写 Refresh TokenToken 过期后需要手动更新。</p>
</div>
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-3">Access Token *</label>
<textarea
v-model="accountForm.accessToken"
rows="4"
class="form-input w-full resize-none font-mono text-sm"
:placeholder="accountForm.platform === 'claude' ? 'sk-ant-oat01-...' : 'ya29.a0A...'"
required
></textarea>
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-3">Refresh Token (可选)</label>
<textarea
v-model="accountForm.refreshToken"
rows="4"
class="form-input w-full resize-none font-mono text-sm"
:placeholder="accountForm.platform === 'claude' ? 'sk-ant-ort01-...' : '1//0g...'"
></textarea>
<p class="text-xs text-gray-500 mt-2">如果有 Refresh Token填写后系统可以自动刷新过期的 Access Token</p>
</div>
</div>
<div class="border-t pt-6">
<label class="block text-sm font-semibold text-gray-700 mb-3">代理设置 (可选)</label>
<p class="text-sm text-gray-500 mb-4">
<span v-if="accountForm.platform === 'claude'">如果需要使用代理访问Claude服务请配置代理信息。OAuth授权也将通过此代理进行。</span>
<span v-else-if="accountForm.platform === 'gemini'">如果需要使用代理访问Gemini服务请配置代理信息。OAuth授权也将通过此代理进行。</span>
</p>
<select
v-model="accountForm.proxyType"
class="form-input w-full"
>
<option value="">不使用代理</option>
<option value="socks5">SOCKS5 代理</option>
<option value="http">HTTP 代理</option>
</select>
</div>
<div v-if="accountForm.proxyType" class="space-y-4 pl-4 border-l-2 border-blue-200">
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">代理主机</label>
<input
v-model="accountForm.proxyHost"
type="text"
class="form-input w-full"
placeholder="127.0.0.1"
>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">代理端口</label>
<input
v-model="accountForm.proxyPort"
type="number"
class="form-input w-full"
placeholder="1080"
>
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">用户名 (可选)</label>
<input
v-model="accountForm.proxyUsername"
type="text"
class="form-input w-full"
placeholder="代理用户名"
>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">密码 (可选)</label>
<input
v-model="accountForm.proxyPassword"
type="password"
class="form-input w-full"
placeholder="代理密码"
>
</div>
</div>
</div>
</div>
<div class="flex gap-3 pt-6">
<button
type="button"
@click="closeCreateAccountModal()"
class="flex-1 px-6 py-3 bg-gray-100 text-gray-700 rounded-xl font-semibold hover:bg-gray-200 transition-colors"
>
取消
</button>
<button
v-if="accountForm.addType === 'oauth'"
type="button"
@click="nextOAuthStep()"
:disabled="!accountForm.name"
class="btn btn-primary flex-1 py-3 px-6 font-semibold"
>
下一步 <i class="fas fa-arrow-right ml-2"></i>
</button>
<button
v-if="accountForm.addType === 'manual'"
type="button"
@click="createManualAccount()"
:disabled="!accountForm.name || !accountForm.accessToken || createAccountLoading"
class="btn btn-primary flex-1 py-3 px-6 font-semibold"
>
<div v-if="createAccountLoading" class="loading-spinner mr-2"></div>
<i v-else class="fas fa-check mr-2"></i>
{{ createAccountLoading ? '创建中...' : '创建账户' }}
</button>
</div>
</div>
<!-- 步骤2: OAuth 授权 -->
<div v-if="oauthStep === 2">
<!-- Claude OAuth 流程 -->
<div v-if="accountForm.platform === 'claude'" class="space-y-6">
<!-- 获取授权URL -->
<div v-if="!oauthData.authUrl" class="text-center py-8">
<div class="w-16 h-16 mx-auto mb-4 bg-blue-100 rounded-full flex items-center justify-center">
<i class="fas fa-link text-blue-600 text-2xl"></i>
</div>
<h5 class="text-lg font-semibold text-gray-900 mb-2">获取授权链接</h5>
<p class="text-gray-600 mb-6">点击下方按钮生成OAuth授权链接</p>
<button
@click="generateAuthUrl()"
:disabled="authUrlLoading"
class="btn btn-primary px-8 py-3 font-semibold"
>
<div v-if="authUrlLoading" class="loading-spinner mr-2"></div>
<i v-else class="fas fa-magic mr-2"></i>
{{ authUrlLoading ? '生成中...' : '生成授权链接' }}
</button>
</div>
<!-- 显示授权URL和输入框 -->
<div v-if="oauthData.authUrl">
<div class="bg-blue-50 border border-blue-200 rounded-xl p-6 mb-6">
<div class="flex items-start gap-3 mb-4">
<div class="w-8 h-8 bg-blue-500 rounded-lg flex items-center justify-center flex-shrink-0 mt-1">
<i class="fas fa-info text-white text-sm"></i>
</div>
<div>
<h5 class="font-semibold text-blue-900 mb-2">操作说明</h5>
<ol class="text-sm text-blue-800 space-y-1 list-decimal list-inside">
<li>点击下方的授权链接在新页面中完成Claude Code登录</li>
<li>点击"授权"按钮同意应用权限</li>
<li>页面会显示一个 <strong>Authorization Code</strong></li>
<li>复制这个 Authorization Code 并粘贴到下方输入框</li>
<li class="text-xs text-blue-600">💡 提示: 请直接粘贴显示的Authorization Code</li>
</ol>
</div>
</div>
</div>
<div class="space-y-4">
<div>
<label class="block text-sm font-semibold text-gray-700 mb-3">授权链接</label>
<div class="flex gap-2">
<input
:value="oauthData.authUrl"
readonly
class="form-input flex-1 font-mono text-sm bg-gray-50"
>
<button
@click="copyToClipboard(oauthData.authUrl)"
class="btn btn-primary px-4 py-2 flex items-center gap-2"
>
<i class="fas fa-copy"></i>复制
</button>
<a
:href="oauthData.authUrl"
target="_blank"
class="btn btn-success px-4 py-2 flex items-center gap-2"
>
<i class="fas fa-external-link-alt"></i>打开
</a>
</div>
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-3">
<i class="fas fa-key text-blue-500 mr-2"></i>Authorization Code
</label>
<textarea
v-model="oauthData.callbackUrl"
rows="3"
class="form-input w-full resize-none font-mono text-sm"
placeholder="粘贴从Claude页面获取的Authorization Code..."
></textarea>
<p class="text-xs text-gray-500 mt-2">
<i class="fas fa-info-circle mr-1"></i>
请粘贴从Claude页面复制的Authorization Code
</p>
</div>
</div>
</div>
</div>
<!-- Gemini OAuth 流程 -->
<div v-else-if="accountForm.platform === 'gemini'" class="space-y-6">
<!-- 获取授权URL -->
<div v-if="!geminiOauthData.authUrl" class="text-center py-8">
<div class="w-16 h-16 mx-auto mb-4 bg-green-100 rounded-full flex items-center justify-center">
<i class="fas fa-link text-green-600 text-2xl"></i>
</div>
<h5 class="text-lg font-semibold text-gray-900 mb-2">获取授权链接</h5>
<p class="text-gray-600 mb-6">点击下方按钮生成Gemini OAuth授权链接</p>
<button
@click="generateAuthUrl()"
:disabled="authUrlLoading"
class="btn btn-primary px-8 py-3 font-semibold"
>
<div v-if="authUrlLoading" class="loading-spinner mr-2"></div>
<i v-else class="fas fa-magic mr-2"></i>
{{ authUrlLoading ? '生成中...' : '生成授权链接' }}
</button>
</div>
<!-- 显示授权URL和轮询状态 -->
<div v-if="geminiOauthData.authUrl">
<div class="bg-green-50 border border-green-200 rounded-xl p-6 mb-6">
<div class="flex items-start gap-3 mb-4">
<div class="w-8 h-8 bg-green-500 rounded-lg flex items-center justify-center flex-shrink-0 mt-1">
<i class="fas fa-info text-white text-sm"></i>
</div>
<div>
<h5 class="font-semibold text-green-900 mb-2">操作说明</h5>
<ol class="text-sm text-green-800 space-y-1 list-decimal list-inside">
<li>点击下方的授权链接在新页面中完成Google账号登录</li>
<li>点击"登录"按钮后可能会加载很慢(这是正常的)</li>
<li>如果超过1分钟还在加载请按 F5 刷新页面</li>
<li>授权完成后会跳转到 http://localhost:45462 (可能显示无法访问)</li>
<li>复制浏览器地址栏的完整链接并粘贴到下方输入框</li>
</ol>
<div class="mt-3 text-xs text-green-700 bg-green-100 rounded-lg p-3">
<i class="fas fa-lightbulb mr-1"></i>
<strong>提示:</strong>如果页面一直无法跳转可以打开浏览器开发者工具F12F5刷新一下授权页再点击页面的登录按钮在"网络"标签中找到以 localhost:45462 开头的请求复制其完整URL。
</div>
</div>
</div>
</div>
<div class="space-y-4">
<div>
<label class="block text-sm font-semibold text-gray-700 mb-3">授权链接</label>
<div class="flex gap-2">
<input
:value="geminiOauthData.authUrl"
readonly
class="form-input flex-1 font-mono text-sm bg-gray-50"
>
<button
@click="copyToClipboard(geminiOauthData.authUrl)"
class="btn btn-primary px-4 py-2 flex items-center gap-2"
>
<i class="fas fa-copy"></i>复制
</button>
<a
:href="geminiOauthData.authUrl"
target="_blank"
class="btn btn-success px-4 py-2 flex items-center gap-2"
>
<i class="fas fa-external-link-alt"></i>打开
</a>
</div>
</div>
<!-- 授权码输入框 -->
<div>
<label class="block text-sm font-semibold text-gray-700 mb-3">
<i class="fas fa-key text-green-500 mr-2"></i>复制oauth后的链接
</label>
<textarea
v-model="geminiOauthData.code"
rows="3"
class="form-input w-full resize-none font-mono text-sm"
placeholder="粘贴以 http://localhost:45462 开头的完整链接..."
></textarea>
<div class="mt-2 space-y-1">
<p class="text-xs text-gray-600">
<i class="fas fa-check-circle text-green-500 mr-1"></i>
支持粘贴完整链接,系统会自动提取授权码
</p>
<p class="text-xs text-gray-600">
<i class="fas fa-check-circle text-green-500 mr-1"></i>
也可以直接粘贴授权码code参数的值
</p>
</div>
</div>
</div>
</div>
</div>
<div class="flex gap-3 pt-6">
<button
type="button"
@click="oauthStep = 1"
class="flex-1 px-6 py-3 bg-gray-100 text-gray-700 rounded-xl font-semibold hover:bg-gray-200 transition-colors"
>
<i class="fas fa-arrow-left mr-2"></i>上一步
</button>
<!-- Claude 完成按钮 -->
<button
v-if="accountForm.platform === 'claude'"
type="button"
@click="createOAuthAccount()"
:disabled="!oauthData.callbackUrl || !oauthData.authUrl || createAccountLoading"
class="btn btn-success flex-1 py-3 px-6 font-semibold"
>
<div v-if="createAccountLoading" class="loading-spinner mr-2"></div>
<i v-else class="fas fa-check mr-2"></i>
{{ createAccountLoading ? '创建中...' : '完成创建' }}
</button>
<!-- Gemini 完成按钮 -->
<button
v-else-if="accountForm.platform === 'gemini'"
type="button"
@click="createGeminiOAuthAccount()"
:disabled="!geminiOauthData.code || !geminiOauthData.authUrl || createAccountLoading"
class="btn btn-success flex-1 py-3 px-6 font-semibold"
>
<div v-if="createAccountLoading" class="loading-spinner mr-2"></div>
<i v-else class="fas fa-check mr-2"></i>
{{ createAccountLoading ? '创建中...' : '使用授权码创建账户' }}
</button>
</div>
</div>
</div>
</div>
<!-- 编辑账户模态框 -->
<div v-if="showEditAccountModal" class="fixed inset-0 modal z-50 flex items-center justify-center p-4">
<div class="modal-content w-full max-w-2xl p-8 mx-auto max-h-[90vh] overflow-y-auto custom-scrollbar">
<div class="flex items-center justify-between mb-6">
<div class="flex items-center gap-3">
<div class="w-10 h-10 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center">
<i class="fas fa-edit text-white"></i>
</div>
<h3 class="text-xl font-bold text-gray-900">编辑账户</h3>
</div>
<button
@click="closeEditAccountModal"
class="text-gray-400 hover:text-gray-600 transition-colors"
>
<i class="fas fa-times text-xl"></i>
</button>
</div>
<div class="space-y-6">
<div>
<label class="block text-sm font-semibold text-gray-700 mb-3">账户名称</label>
<input
v-model="editAccountForm.name"
type="text"
required
class="form-input w-full"
placeholder="为账户设置一个易识别的名称"
>
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-3">描述 (可选)</label>
<textarea
v-model="editAccountForm.description"
rows="3"
class="form-input w-full resize-none"
placeholder="账户用途说明..."
></textarea>
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-3">账户类型</label>
<div class="flex gap-4">
<label class="flex items-center cursor-pointer">
<input
type="radio"
v-model="editAccountForm.accountType"
value="shared"
class="mr-2"
>
<span class="text-sm text-gray-700">共享账户</span>
</label>
<label class="flex items-center cursor-pointer">
<input
type="radio"
v-model="editAccountForm.accountType"
value="dedicated"
class="mr-2"
>
<span class="text-sm text-gray-700">专属账户</span>
</label>
</div>
<div v-if="editAccountForm.accountType === 'shared' && editAccountForm.originalAccountType === 'dedicated'"
class="mt-2 p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
<div class="flex items-start gap-2">
<i class="fas fa-exclamation-triangle text-yellow-600 mt-0.5"></i>
<div>
<p class="text-sm text-yellow-800 font-medium">切换到共享账户需要验证</p>
<p class="text-xs text-yellow-700 mt-1">当前账户绑定了 {{ getBoundApiKeysCount(editAccountForm.id) }} 个API Key需要先解绑所有API Key才能切换到共享账户。</p>
</div>
</div>
</div>
<p class="text-xs text-gray-500 mt-2">
共享账户供所有API Key使用专属账户仅供特定API Key使用
</p>
</div>
<!-- Token 更新区域 -->
<!-- Gemini 项目编号字段(编辑模式) -->
<div v-if="editAccountForm.platform === 'gemini'">
<label class="block text-sm font-semibold text-gray-700 mb-3">项目编号 (可选)</label>
<input
v-model="editAccountForm.projectId"
type="text"
class="form-input w-full"
placeholder="例如123456789012纯数字"
>
<div class="mt-2 p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
<div class="flex items-start gap-2">
<i class="fas fa-info-circle text-yellow-600 mt-0.5"></i>
<div class="text-xs text-yellow-700">
<p class="font-medium mb-1">Google Cloud/Workspace 账号需要提供项目编号</p>
<p>如果您的账号被识别为 Workspace 账号,请提供项目编号。留空将尝试自动检测。</p>
<div class="mt-2 p-2 bg-white rounded border border-yellow-300">
<p class="font-medium mb-1">如何获取项目编号:</p>
<ol class="list-decimal list-inside space-y-1 ml-2">
<li>访问 <a href="https://console.cloud.google.com/welcome" target="_blank" class="text-blue-600 hover:underline font-medium">Google Cloud Console</a></li>
<li>复制<span class="font-semibold text-red-600">项目编号Project Number</span>通常是12位纯数字</li>
<li class="text-red-600">⚠️ 注意不要复制项目IDProject ID要复制项目编号</li>
</ol>
</div>
<p class="mt-2"><strong>提示:</strong>如果您的账号是普通个人账号(未绑定 Google Cloud请留空此字段。</p>
</div>
</div>
</div>
</div>
<div class="bg-amber-50 p-4 rounded-lg border border-amber-200">
<div class="flex items-start gap-3 mb-4">
<div class="w-8 h-8 bg-amber-500 rounded-lg flex items-center justify-center flex-shrink-0 mt-1">
<i class="fas fa-key text-white text-sm"></i>
</div>
<div>
<h5 class="font-semibold text-amber-900 mb-2">更新 Token</h5>
<p class="text-sm text-amber-800 mb-2">可以更新 Access Token 和 Refresh Token。为了安全起见不会显示当前的 Token 值。</p>
<p class="text-xs text-amber-600">💡 留空表示不更新该字段。</p>
</div>
</div>
<div class="space-y-4">
<div>
<label class="block text-sm font-semibold text-gray-700 mb-3">Access Token</label>
<textarea
v-model="editAccountForm.accessToken"
rows="4"
class="form-input w-full resize-none font-mono text-sm"
placeholder="留空表示不更新,否则输入新的 Access Token"
></textarea>
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-3">Refresh Token</label>
<textarea
v-model="editAccountForm.refreshToken"
rows="4"
class="form-input w-full resize-none font-mono text-sm"
placeholder="留空表示不更新,否则输入新的 Refresh Token"
></textarea>
<p class="text-xs text-gray-500 mt-2">如果有 Refresh Token填写后系统可以自动刷新过期的 Access Token</p>
</div>
</div>
</div>
<!-- 代理设置 -->
<div class="border-t pt-6">
<label class="block text-sm font-semibold text-gray-700 mb-3">代理设置 (可选)</label>
<p class="text-sm text-gray-500 mb-4">如果需要修改代理设置,请更新代理信息。</p>
<select
v-model="editAccountForm.proxyType"
class="form-input w-full"
>
<option value="">不使用代理</option>
<option value="socks5">SOCKS5 代理</option>
<option value="http">HTTP 代理</option>
</select>
</div>
<div v-if="editAccountForm.proxyType" class="space-y-4 pl-4 border-l-2 border-blue-200">
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">代理主机</label>
<input
v-model="editAccountForm.proxyHost"
type="text"
class="form-input w-full"
placeholder="127.0.0.1"
>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">代理端口</label>
<input
v-model="editAccountForm.proxyPort"
type="number"
class="form-input w-full"
placeholder="1080"
>
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">用户名 (可选)</label>
<input
v-model="editAccountForm.proxyUsername"
type="text"
class="form-input w-full"
placeholder="代理用户名"
>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">密码 (可选)</label>
<input
v-model="editAccountForm.proxyPassword"
type="password"
class="form-input w-full"
placeholder="代理密码"
>
</div>
</div>
</div>
</div>
<div class="flex gap-3 pt-6">
<button
type="button"
@click="closeEditAccountModal"
class="flex-1 px-6 py-3 bg-gray-100 text-gray-700 rounded-xl font-semibold hover:bg-gray-200 transition-colors"
>
取消
</button>
<button
type="button"
@click="updateAccount()"
:disabled="!editAccountForm.name || editAccountLoading"
class="btn btn-primary flex-1 py-3 px-6 font-semibold"
>
<div v-if="editAccountLoading" class="loading-spinner mr-2"></div>
<i v-else class="fas fa-save mr-2"></i>
{{ editAccountLoading ? '更新中...' : '保存修改' }}
</button>
</div>
</div>
</div>
<!-- 修改账户信息模态框 -->
<div v-if="showChangePasswordModal" class="fixed inset-0 modal z-50 flex items-center justify-center p-4">
<div class="modal-content w-full max-w-md p-8 mx-auto max-h-[90vh] flex flex-col">
<div class="flex items-center justify-between mb-6">
<div class="flex items-center gap-3">
<div class="w-10 h-10 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center">
<i class="fas fa-key text-white"></i>
</div>
<h3 class="text-xl font-bold text-gray-900">修改账户信息</h3>
</div>
<button
@click="closeChangePasswordModal"
class="text-gray-400 hover:text-gray-600 transition-colors"
>
<i class="fas fa-times text-xl"></i>
</button>
</div>
<form @submit.prevent="changePassword" class="space-y-6 modal-scroll-content custom-scrollbar flex-1">
<div>
<label class="block text-sm font-semibold text-gray-700 mb-3">当前用户名</label>
<input
:value="currentUser.username || 'Admin'"
type="text"
disabled
class="form-input w-full bg-gray-100 cursor-not-allowed"
>
<p class="text-xs text-gray-500 mt-2">当前用户名,输入新用户名以修改</p>
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-3">新用户名</label>
<input
v-model="changePasswordForm.newUsername"
type="text"
class="form-input w-full"
placeholder="输入新用户名(留空保持不变)"
>
<p class="text-xs text-gray-500 mt-2">留空表示不修改用户名</p>
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-3">当前密码</label>
<input
v-model="changePasswordForm.currentPassword"
type="password"
required
class="form-input w-full"
placeholder="请输入当前密码"
>
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-3">新密码</label>
<input
v-model="changePasswordForm.newPassword"
type="password"
required
class="form-input w-full"
placeholder="请输入新密码"
>
<p class="text-xs text-gray-500 mt-2">密码长度至少8位</p>
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-3">确认新密码</label>
<input
v-model="changePasswordForm.confirmPassword"
type="password"
required
class="form-input w-full"
placeholder="请再次输入新密码"
>
</div>
<div class="flex gap-3 pt-4">
<button
type="button"
@click="closeChangePasswordModal"
class="flex-1 px-6 py-3 bg-gray-100 text-gray-700 rounded-xl font-semibold hover:bg-gray-200 transition-colors"
>
取消
</button>
<button
type="submit"
:disabled="changePasswordLoading"
class="btn btn-primary flex-1 py-3 px-6 font-semibold"
>
<div v-if="changePasswordLoading" class="loading-spinner mr-2"></div>
<i v-else class="fas fa-save mr-2"></i>
{{ changePasswordLoading ? '保存中...' : '保存修改' }}
</button>
</div>
</form>
</div>
</div>
<!-- 确认弹窗 -->
<div v-if="showConfirmModal" class="fixed inset-0 modal z-50 flex items-center justify-center p-4">
<div class="modal-content w-full max-w-md p-6 mx-auto">
<div class="flex items-start gap-4 mb-6">
<div class="w-12 h-12 bg-gradient-to-br from-yellow-400 to-yellow-500 rounded-full flex items-center justify-center flex-shrink-0">
<i class="fas fa-exclamation text-white text-xl"></i>
</div>
<div class="flex-1">
<h3 class="text-lg font-bold text-gray-900 mb-2">{{ confirmModal.title }}</h3>
<p class="text-gray-600 text-sm leading-relaxed whitespace-pre-line">{{ confirmModal.message }}</p>
</div>
</div>
<div class="flex gap-3">
<button
@click="handleConfirmCancel"
class="flex-1 px-4 py-2.5 bg-gray-100 text-gray-700 rounded-xl font-medium hover:bg-gray-200 transition-colors"
>
{{ confirmModal.cancelText || '取消' }}
</button>
<button
@click="handleConfirmOk"
class="flex-1 px-4 py-2.5 bg-gradient-to-r from-yellow-500 to-orange-500 text-white rounded-xl font-medium hover:from-yellow-600 hover:to-orange-600 transition-colors shadow-sm"
>
{{ confirmModal.confirmText || '继续' }}
</button>
</div>
</div>
</div>
<!-- Toast 通知组件 -->
<div v-for="(toast, index) in toasts" :key="toast.id"
:class="['toast rounded-2xl p-4 shadow-2xl backdrop-blur-sm',
'toast-' + toast.type,
toast.show ? 'show' : '']"
:style="{ top: (80 + index * 80) + 'px' }">
<div class="flex items-start gap-3">
<div class="flex-shrink-0 mt-0.5">
<i :class="getToastIcon(toast.type) + ' text-lg'"></i>
</div>
<div class="flex-1 min-w-0">
<h4 v-if="toast.title" class="font-semibold text-sm mb-1">{{ toast.title }}</h4>
<p class="text-sm opacity-90 leading-relaxed">{{ toast.message }}</p>
</div>
<button @click="removeToast(toast.id)"
class="flex-shrink-0 text-white/70 hover:text-white transition-colors ml-2">
<i class="fas fa-times"></i>
</button>
</div>
</div>
</div>
<script src="/web/app.js"></script>
</body>
</html>