# Conflicts:
#	web/admin-spa/dist/assets/LoginView-BJ0LLv16.js
#	web/admin-spa/dist/assets/LogoTitle-DHj-MjwS.js
#	web/admin-spa/dist/assets/MainLayout-CLydIeqJ.js
#	web/admin-spa/dist/assets/SettingsView-DicW12bL.js
#	web/admin-spa/dist/assets/index-HYE9xPuR.js
#	web/admin-spa/dist/index.html
#	web/admin-spa/src/components/apikeys/CreateApiKeyModal.vue
#	web/admin-spa/src/components/apikeys/EditApiKeyModal.vue
#	web/admin-spa/src/views/ApiKeysView.vue
This commit is contained in:
KevinLiao
2025-07-30 20:41:10 +08:00
49 changed files with 4461 additions and 2427 deletions

View File

@@ -1,36 +1,48 @@
<template>
<Teleport to="body">
<div v-if="show" class="fixed inset-0 modal z-50 flex items-center justify-center p-4">
<div
v-if="show"
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>
<i class="fas fa-user-circle text-white" />
</div>
<h3 class="text-xl font-bold text-gray-900">{{ isEdit ? '编辑账户' : '添加账户' }}</h3>
<h3 class="text-xl font-bold text-gray-900">
{{ isEdit ? '编辑账户' : '添加账户' }}
</h3>
</div>
<button
@click="$emit('close')"
class="text-gray-400 hover:text-gray-600 transition-colors"
@click="$emit('close')"
>
<i class="fas fa-times text-xl"></i>
<i class="fas fa-times text-xl" />
</button>
</div>
<!-- 步骤指示器 -->
<div v-if="!isEdit && form.addType === 'oauth'" class="flex items-center justify-center mb-8">
<div
v-if="!isEdit && form.addType === 'oauth'"
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']">
<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="w-8 h-0.5 bg-gray-300" />
<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']">
<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>
@@ -46,8 +58,8 @@
<div class="flex gap-4">
<label class="flex items-center cursor-pointer">
<input
type="radio"
v-model="form.platform"
type="radio"
value="claude"
class="mr-2"
>
@@ -55,8 +67,8 @@
</label>
<label class="flex items-center cursor-pointer">
<input
type="radio"
v-model="form.platform"
type="radio"
value="claude-console"
class="mr-2"
>
@@ -64,8 +76,8 @@
</label>
<label class="flex items-center cursor-pointer">
<input
type="radio"
v-model="form.platform"
type="radio"
value="gemini"
class="mr-2"
>
@@ -79,8 +91,8 @@
<div class="flex gap-4">
<label class="flex items-center cursor-pointer">
<input
type="radio"
v-model="form.addType"
type="radio"
value="oauth"
class="mr-2"
>
@@ -88,8 +100,8 @@
</label>
<label class="flex items-center cursor-pointer">
<input
type="radio"
v-model="form.addType"
type="radio"
value="manual"
class="mr-2"
>
@@ -108,7 +120,12 @@
:class="{ 'border-red-500': errors.name }"
placeholder="为账户设置一个易识别的名称"
>
<p v-if="errors.name" class="text-red-500 text-xs mt-1">{{ errors.name }}</p>
<p
v-if="errors.name"
class="text-red-500 text-xs mt-1"
>
{{ errors.name }}
</p>
</div>
<div>
@@ -118,7 +135,7 @@
rows="3"
class="form-input w-full resize-none"
placeholder="账户用途说明..."
></textarea>
/>
</div>
<div>
@@ -126,8 +143,8 @@
<div class="flex gap-4">
<label class="flex items-center cursor-pointer">
<input
type="radio"
v-model="form.accountType"
type="radio"
value="shared"
class="mr-2"
>
@@ -135,8 +152,8 @@
</label>
<label class="flex items-center cursor-pointer">
<input
type="radio"
v-model="form.accountType"
type="radio"
value="dedicated"
class="mr-2"
>
@@ -159,26 +176,43 @@
>
<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>
<i class="fas fa-info-circle text-yellow-600 mt-0.5" />
<div class="text-xs text-yellow-700">
<p class="font-medium mb-1">Google Cloud/Workspace 账号需要提供项目编号</p>
<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>
<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>
访问 <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>
<li class="text-red-600">
注意不要复制项目IDProject ID要复制项目编号
</li>
</ol>
</div>
<p class="mt-2"><strong>提示</strong>如果您的账号是普通个人账号未绑定 Google Cloud请留空此字段</p>
<p class="mt-2">
<strong>提示</strong>如果您的账号是普通个人账号未绑定 Google Cloud请留空此字段
</p>
</div>
</div>
</div>
</div>
<!-- Claude Console 特定字段 -->
<div v-if="form.platform === 'claude-console' && !isEdit" class="space-y-4">
<div
v-if="form.platform === 'claude-console' && !isEdit"
class="space-y-4"
>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-3">API URL *</label>
<input
@@ -189,7 +223,12 @@
:class="{ 'border-red-500': errors.apiUrl }"
placeholder="例如https://api.example.com"
>
<p v-if="errors.apiUrl" class="text-red-500 text-xs mt-1">{{ errors.apiUrl }}</p>
<p
v-if="errors.apiUrl"
class="text-red-500 text-xs mt-1"
>
{{ errors.apiUrl }}
</p>
</div>
<div>
@@ -202,7 +241,12 @@
:class="{ 'border-red-500': errors.apiKey }"
placeholder="请输入API Key"
>
<p v-if="errors.apiKey" class="text-red-500 text-xs mt-1">{{ errors.apiKey }}</p>
<p
v-if="errors.apiKey"
class="text-red-500 text-xs mt-1"
>
{{ errors.apiKey }}
</p>
</div>
<div>
@@ -210,22 +254,22 @@
<div class="mb-2 flex gap-2">
<button
type="button"
@click="addPresetModel('claude-sonnet-4-20250514')"
class="px-3 py-1 text-xs bg-blue-100 text-blue-700 rounded-lg hover:bg-blue-200 transition-colors"
@click="addPresetModel('claude-sonnet-4-20250514')"
>
+ claude-sonnet-4-20250514
</button>
<button
type="button"
@click="addPresetModel('claude-opus-4-20250514')"
class="px-3 py-1 text-xs bg-purple-100 text-purple-700 rounded-lg hover:bg-purple-200 transition-colors"
@click="addPresetModel('claude-opus-4-20250514')"
>
+ claude-opus-4-20250514
</button>
<button
type="button"
@click="addPresetModel('claude-3-5-haiku-20241022')"
class="px-3 py-1 text-xs bg-green-100 text-green-700 rounded-lg hover:bg-purple-200 transition-colors"
@click="addPresetModel('claude-3-5-haiku-20241022')"
>
+ claude-3-5-haiku-20241022
</button>
@@ -235,8 +279,10 @@
rows="3"
class="form-input w-full resize-none"
placeholder="每行一个模型,留空表示支持所有模型。特别注意,ClaudeCode必须加上hiku模型"
></textarea>
<p class="text-xs text-gray-500 mt-1">留空表示支持所有模型如果指定模型请求中的模型不在列表内将不会调度到此账号</p>
/>
<p class="text-xs text-gray-500 mt-1">
留空表示支持所有模型如果指定模型请求中的模型不在列表内将不会调度到此账号
</p>
</div>
<div>
@@ -258,7 +304,9 @@
class="form-input w-full"
placeholder="默认60分钟"
>
<p class="text-xs text-gray-500 mt-1">当账号返回429错误时暂停调度的时间分钟</p>
<p class="text-xs text-gray-500 mt-1">
当账号返回429错误时暂停调度的时间分钟
</p>
</div>
</div>
@@ -273,37 +321,58 @@
class="form-input w-full"
placeholder="数字越小优先级越高默认50"
>
<p class="text-xs text-gray-500 mt-1">数字越小优先级越高建议范围1-100</p>
<p class="text-xs text-gray-500 mt-1">
数字越小优先级越高建议范围1-100
</p>
</div>
<!-- 手动输入 Token 字段 -->
<div v-if="form.addType === 'manual' && form.platform !== 'claude-console'" class="space-y-4 bg-blue-50 p-4 rounded-lg border border-blue-200">
<div
v-if="form.addType === 'manual' && form.platform !== 'claude-console'"
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>
<i class="fas fa-info text-white text-sm" />
</div>
<div>
<h5 class="font-semibold text-blue-900 mb-2">手动输入 Token</h5>
<p v-if="form.platform === 'claude'" class="text-sm text-blue-800 mb-2">
<h5 class="font-semibold text-blue-900 mb-2">
手动输入 Token
</h5>
<p
v-if="form.platform === 'claude'"
class="text-sm text-blue-800 mb-2"
>
请输入有效的 Claude Access Token如果您有 Refresh Token建议也一并填写以支持自动刷新
</p>
<p v-else-if="form.platform === 'gemini'" class="text-sm text-blue-800 mb-2">
<p
v-else-if="form.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>
<i class="fas fa-folder-open mr-1" />
获取 Access Token 的方法
</p>
<p v-if="form.platform === 'claude'" class="text-xs text-blue-800">
<p
v-if="form.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="form.platform === 'gemini'" class="text-xs text-blue-800">
<p
v-else-if="form.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>
<p class="text-xs text-blue-600">
💡 如果未填写 Refresh TokenToken 过期后需要手动更新
</p>
</div>
</div>
@@ -316,8 +385,13 @@
class="form-input w-full resize-none font-mono text-xs"
:class="{ 'border-red-500': errors.accessToken }"
placeholder="请输入 Access Token..."
></textarea>
<p v-if="errors.accessToken" class="text-red-500 text-xs mt-1">{{ errors.accessToken }}</p>
/>
<p
v-if="errors.accessToken"
class="text-red-500 text-xs mt-1"
>
{{ errors.accessToken }}
</p>
</div>
<div>
@@ -327,7 +401,7 @@
rows="4"
class="form-input w-full resize-none font-mono text-xs"
placeholder="请输入 Refresh Token..."
></textarea>
/>
</div>
</div>
@@ -337,28 +411,31 @@
<div class="flex gap-3 pt-4">
<button
type="button"
@click="$emit('close')"
class="flex-1 px-6 py-3 bg-gray-100 text-gray-700 rounded-xl font-semibold hover:bg-gray-200 transition-colors"
class="flex-1 px-6 py-3 bg-gray-100 text-gray-700 rounded-xl font-semibold hover:bg-gray-200 transition-colors"
@click="$emit('close')"
>
取消
</button>
<button
v-if="form.addType === 'oauth' && form.platform !== 'claude-console'"
type="button"
@click="nextStep"
:disabled="loading"
class="btn btn-primary flex-1 py-3 px-6 font-semibold"
@click="nextStep"
>
下一步
</button>
<button
v-else
type="button"
@click="createAccount"
:disabled="loading"
class="btn btn-primary flex-1 py-3 px-6 font-semibold"
@click="createAccount"
>
<div v-if="loading" class="loading-spinner mr-2"></div>
<div
v-if="loading"
class="loading-spinner mr-2"
/>
{{ loading ? '创建中...' : '创建' }}
</button>
</div>
@@ -375,7 +452,10 @@
/>
<!-- 编辑模式 -->
<div v-if="isEdit" class="space-y-6">
<div
v-if="isEdit"
class="space-y-6"
>
<!-- 基本信息 -->
<div>
<label class="block text-sm font-semibold text-gray-700 mb-3">账户名称</label>
@@ -395,7 +475,7 @@
rows="3"
class="form-input w-full resize-none"
placeholder="账户用途说明..."
></textarea>
/>
</div>
<div>
@@ -403,8 +483,8 @@
<div class="flex gap-4">
<label class="flex items-center cursor-pointer">
<input
type="radio"
v-model="form.accountType"
type="radio"
value="shared"
class="mr-2"
>
@@ -412,8 +492,8 @@
</label>
<label class="flex items-center cursor-pointer">
<input
type="radio"
v-model="form.accountType"
type="radio"
value="dedicated"
class="mr-2"
>
@@ -450,11 +530,16 @@
class="form-input w-full"
placeholder="数字越小优先级越高"
>
<p class="text-xs text-gray-500 mt-1">数字越小优先级越高建议范围1-100</p>
<p class="text-xs text-gray-500 mt-1">
数字越小优先级越高建议范围1-100
</p>
</div>
<!-- Claude Console 特定字段编辑模式-->
<div v-if="form.platform === 'claude-console'" class="space-y-4">
<div
v-if="form.platform === 'claude-console'"
class="space-y-4"
>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-3">API URL</label>
<input
@@ -474,7 +559,9 @@
class="form-input w-full"
placeholder="留空表示不更新"
>
<p class="text-xs text-gray-500 mt-1">留空表示不更新 API Key</p>
<p class="text-xs text-gray-500 mt-1">
留空表示不更新 API Key
</p>
</div>
<div>
@@ -482,32 +569,32 @@
<div class="mb-2 flex gap-2">
<button
type="button"
@click="addPresetModel('claude-sonnet-4-20250514')"
class="px-3 py-1 text-xs bg-blue-100 text-blue-700 rounded-lg hover:bg-blue-200 transition-colors"
@click="addPresetModel('claude-sonnet-4-20250514')"
>
+ claude-sonnet-4-20250514
</button>
<button
type="button"
@click="addPresetModel('claude-opus-4-20250514')"
class="px-3 py-1 text-xs bg-purple-100 text-purple-700 rounded-lg hover:bg-purple-200 transition-colors"
@click="addPresetModel('claude-opus-4-20250514')"
>
+ claude-opus-4-20250514
</button>
<button
type="button"
@click="addPresetModel('claude-3-5-haiku-20241022')"
class="px-3 py-1 text-xs bg-green-100 text-green-700 rounded-lg hover:bg-purple-200 transition-colors"
>
+ claude-3-5-haiku-20241022
</button>
type="button"
class="px-3 py-1 text-xs bg-green-100 text-green-700 rounded-lg hover:bg-purple-200 transition-colors"
@click="addPresetModel('claude-3-5-haiku-20241022')"
>
+ claude-3-5-haiku-20241022
</button>
</div>
<textarea
v-model="form.supportedModels"
rows="3"
class="form-input w-full resize-none"
placeholder="每行一个模型,留空表示支持所有模型。特别注意,ClaudeCode必须加上hiku模型"
></textarea>
/>
</div>
<div>
@@ -532,15 +619,24 @@
</div>
<!-- Token 更新 -->
<div v-if="form.platform !== 'claude-console'" class="bg-amber-50 p-4 rounded-lg border border-amber-200">
<div
v-if="form.platform !== 'claude-console'"
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>
<i class="fas fa-key text-white text-sm" />
</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>
<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>
@@ -552,7 +648,7 @@
rows="4"
class="form-input w-full resize-none font-mono text-xs"
placeholder="留空表示不更新..."
></textarea>
/>
</div>
<div>
@@ -562,7 +658,7 @@
rows="4"
class="form-input w-full resize-none font-mono text-xs"
placeholder="留空表示不更新..."
></textarea>
/>
</div>
</div>
</div>
@@ -573,18 +669,21 @@
<div class="flex gap-3 pt-4">
<button
type="button"
@click="$emit('close')"
class="flex-1 px-6 py-3 bg-gray-100 text-gray-700 rounded-xl font-semibold hover:bg-gray-200 transition-colors"
class="flex-1 px-6 py-3 bg-gray-100 text-gray-700 rounded-xl font-semibold hover:bg-gray-200 transition-colors"
@click="$emit('close')"
>
取消
</button>
<button
type="button"
@click="updateAccount"
:disabled="loading"
class="btn btn-primary flex-1 py-3 px-6 font-semibold"
@click="updateAccount"
>
<div v-if="loading" class="loading-spinner mr-2"></div>
<div
v-if="loading"
class="loading-spinner mr-2"
/>
{{ loading ? '更新中...' : '更新' }}
</button>
</div>

View File

@@ -5,10 +5,12 @@
<div class="bg-blue-50 p-6 rounded-lg border border-blue-200">
<div class="flex items-start gap-4">
<div class="w-10 h-10 bg-blue-500 rounded-lg flex items-center justify-center flex-shrink-0">
<i class="fas fa-link text-white"></i>
<i class="fas fa-link text-white" />
</div>
<div class="flex-1">
<h4 class="font-semibold text-blue-900 mb-3">Claude 账户授权</h4>
<h4 class="font-semibold text-blue-900 mb-3">
Claude 账户授权
</h4>
<p class="text-sm text-blue-800 mb-4">
请按照以下步骤完成 Claude 账户的授权
</p>
@@ -17,20 +19,33 @@
<!-- 步骤1: 生成授权链接 -->
<div class="bg-white/80 rounded-lg p-4 border border-blue-300">
<div class="flex items-start gap-3">
<div class="w-6 h-6 bg-blue-600 text-white rounded-full flex items-center justify-center text-xs font-bold flex-shrink-0">1</div>
<div class="w-6 h-6 bg-blue-600 text-white rounded-full flex items-center justify-center text-xs font-bold flex-shrink-0">
1
</div>
<div class="flex-1">
<p class="font-medium text-blue-900 mb-2">点击下方按钮生成授权链接</p>
<p class="font-medium text-blue-900 mb-2">
点击下方按钮生成授权链接
</p>
<button
v-if="!authUrl"
@click="generateAuthUrl"
:disabled="loading"
class="btn btn-primary px-4 py-2 text-sm"
@click="generateAuthUrl"
>
<i v-if="!loading" class="fas fa-link mr-2"></i>
<div v-else class="loading-spinner mr-2"></div>
<i
v-if="!loading"
class="fas fa-link mr-2"
/>
<div
v-else
class="loading-spinner mr-2"
/>
{{ loading ? '生成中...' : '生成授权链接' }}
</button>
<div v-else class="space-y-3">
<div
v-else
class="space-y-3"
>
<div class="flex items-center gap-2">
<input
type="text"
@@ -39,18 +54,18 @@
class="form-input flex-1 text-xs font-mono bg-gray-50"
>
<button
@click="copyAuthUrl"
class="px-3 py-2 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
title="复制链接"
@click="copyAuthUrl"
>
<i :class="copied ? 'fas fa-check text-green-500' : 'fas fa-copy'"></i>
<i :class="copied ? 'fas fa-check text-green-500' : 'fas fa-copy'" />
</button>
</div>
<button
@click="regenerateAuthUrl"
class="text-xs text-blue-600 hover:text-blue-700"
@click="regenerateAuthUrl"
>
<i class="fas fa-sync-alt mr-1"></i>重新生成
<i class="fas fa-sync-alt mr-1" />重新生成
</button>
</div>
</div>
@@ -60,15 +75,19 @@
<!-- 步骤2: 访问链接并授权 -->
<div class="bg-white/80 rounded-lg p-4 border border-blue-300">
<div class="flex items-start gap-3">
<div class="w-6 h-6 bg-blue-600 text-white rounded-full flex items-center justify-center text-xs font-bold flex-shrink-0">2</div>
<div class="w-6 h-6 bg-blue-600 text-white rounded-full flex items-center justify-center text-xs font-bold flex-shrink-0">
2
</div>
<div class="flex-1">
<p class="font-medium text-blue-900 mb-2">在浏览器中打开链接并完成授权</p>
<p class="font-medium text-blue-900 mb-2">
在浏览器中打开链接并完成授权
</p>
<p class="text-sm text-blue-700 mb-2">
请在新标签页中打开授权链接登录您的 Claude 账户并授权
</p>
<div class="bg-yellow-50 p-3 rounded border border-yellow-300">
<p class="text-xs text-yellow-800">
<i class="fas fa-exclamation-triangle mr-1"></i>
<i class="fas fa-exclamation-triangle mr-1" />
<strong>注意</strong>如果您设置了代理请确保浏览器也使用相同的代理访问授权页面
</p>
</div>
@@ -79,26 +98,30 @@
<!-- 步骤3: 输入授权码 -->
<div class="bg-white/80 rounded-lg p-4 border border-blue-300">
<div class="flex items-start gap-3">
<div class="w-6 h-6 bg-blue-600 text-white rounded-full flex items-center justify-center text-xs font-bold flex-shrink-0">3</div>
<div class="w-6 h-6 bg-blue-600 text-white rounded-full flex items-center justify-center text-xs font-bold flex-shrink-0">
3
</div>
<div class="flex-1">
<p class="font-medium text-blue-900 mb-2">输入 Authorization Code</p>
<p class="font-medium text-blue-900 mb-2">
输入 Authorization Code
</p>
<p class="text-sm text-blue-700 mb-3">
授权完成后页面会显示一个 <strong>Authorization Code</strong>请将其复制并粘贴到下方输入框
</p>
<div class="space-y-3">
<div>
<label class="block text-sm font-semibold text-gray-700 mb-2">
<i class="fas fa-key text-blue-500 mr-2"></i>Authorization Code
<i class="fas fa-key text-blue-500 mr-2" />Authorization Code
</label>
<textarea
v-model="authCode"
rows="3"
class="form-input w-full resize-none font-mono text-sm"
placeholder="粘贴从Claude页面获取的Authorization Code..."
></textarea>
/>
</div>
<p class="text-xs text-gray-500 mt-2">
<i class="fas fa-info-circle mr-1"></i>
<i class="fas fa-info-circle mr-1" />
请粘贴从Claude页面复制的Authorization Code
</p>
</div>
@@ -116,10 +139,12 @@
<div class="bg-green-50 p-6 rounded-lg border border-green-200">
<div class="flex items-start gap-4">
<div class="w-10 h-10 bg-green-500 rounded-lg flex items-center justify-center flex-shrink-0">
<i class="fas fa-robot text-white"></i>
<i class="fas fa-robot text-white" />
</div>
<div class="flex-1">
<h4 class="font-semibold text-green-900 mb-3">Gemini 账户授权</h4>
<h4 class="font-semibold text-green-900 mb-3">
Gemini 账户授权
</h4>
<p class="text-sm text-green-800 mb-4">
请按照以下步骤完成 Gemini 账户的授权
</p>
@@ -128,20 +153,33 @@
<!-- 步骤1: 生成授权链接 -->
<div class="bg-white/80 rounded-lg p-4 border border-green-300">
<div class="flex items-start gap-3">
<div class="w-6 h-6 bg-green-600 text-white rounded-full flex items-center justify-center text-xs font-bold flex-shrink-0">1</div>
<div class="w-6 h-6 bg-green-600 text-white rounded-full flex items-center justify-center text-xs font-bold flex-shrink-0">
1
</div>
<div class="flex-1">
<p class="font-medium text-green-900 mb-2">点击下方按钮生成授权链接</p>
<p class="font-medium text-green-900 mb-2">
点击下方按钮生成授权链接
</p>
<button
v-if="!authUrl"
@click="generateAuthUrl"
:disabled="loading"
class="btn btn-primary px-4 py-2 text-sm"
@click="generateAuthUrl"
>
<i v-if="!loading" class="fas fa-link mr-2"></i>
<div v-else class="loading-spinner mr-2"></div>
<i
v-if="!loading"
class="fas fa-link mr-2"
/>
<div
v-else
class="loading-spinner mr-2"
/>
{{ loading ? '生成中...' : '生成授权链接' }}
</button>
<div v-else class="space-y-3">
<div
v-else
class="space-y-3"
>
<div class="flex items-center gap-2">
<input
type="text"
@@ -150,18 +188,18 @@
class="form-input flex-1 text-xs font-mono bg-gray-50"
>
<button
@click="copyAuthUrl"
class="px-3 py-2 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
title="复制链接"
@click="copyAuthUrl"
>
<i :class="copied ? 'fas fa-check text-green-500' : 'fas fa-copy'"></i>
<i :class="copied ? 'fas fa-check text-green-500' : 'fas fa-copy'" />
</button>
</div>
<button
@click="regenerateAuthUrl"
class="text-xs text-green-600 hover:text-green-700"
@click="regenerateAuthUrl"
>
<i class="fas fa-sync-alt mr-1"></i>重新生成
<i class="fas fa-sync-alt mr-1" />重新生成
</button>
</div>
</div>
@@ -171,9 +209,13 @@
<!-- 步骤2: 操作说明 -->
<div class="bg-white/80 rounded-lg p-4 border border-green-300">
<div class="flex items-start gap-3">
<div class="w-6 h-6 bg-green-600 text-white rounded-full flex items-center justify-center text-xs font-bold flex-shrink-0">2</div>
<div class="w-6 h-6 bg-green-600 text-white rounded-full flex items-center justify-center text-xs font-bold flex-shrink-0">
2
</div>
<div class="flex-1">
<p class="font-medium text-green-900 mb-2">在浏览器中打开链接并完成授权</p>
<p class="font-medium text-green-900 mb-2">
在浏览器中打开链接并完成授权
</p>
<ol class="text-sm text-green-800 space-y-1 list-decimal list-inside mb-3">
<li>点击上方的授权链接在新页面中完成Google账号登录</li>
<li>点击登录按钮后可能会加载很慢这是正常的</li>
@@ -182,7 +224,7 @@
</ol>
<div class="bg-green-100 p-3 rounded border border-green-300">
<p class="text-xs text-green-700">
<i class="fas fa-lightbulb mr-1"></i>
<i class="fas fa-lightbulb mr-1" />
<strong>提示</strong>如果页面一直无法跳转可以打开浏览器开发者工具F12F5刷新一下授权页再点击页面的登录按钮网络标签中找到以 localhost:45462 开头的请求复制其完整URL
</p>
</div>
@@ -193,31 +235,35 @@
<!-- 步骤3: 输入授权码 -->
<div class="bg-white/80 rounded-lg p-4 border border-green-300">
<div class="flex items-start gap-3">
<div class="w-6 h-6 bg-green-600 text-white rounded-full flex items-center justify-center text-xs font-bold flex-shrink-0">3</div>
<div class="w-6 h-6 bg-green-600 text-white rounded-full flex items-center justify-center text-xs font-bold flex-shrink-0">
3
</div>
<div class="flex-1">
<p class="font-medium text-green-900 mb-2">复制oauth后的链接</p>
<p class="font-medium text-green-900 mb-2">
复制oauth后的链接
</p>
<p class="text-sm text-green-700 mb-3">
复制浏览器地址栏的完整链接并粘贴到下方输入框
</p>
<div class="space-y-3">
<div>
<label class="block text-sm font-semibold text-gray-700 mb-2">
<i class="fas fa-key text-green-500 mr-2"></i>复制oauth后的链接
<i class="fas fa-key text-green-500 mr-2" />复制oauth后的链接
</label>
<textarea
v-model="authCode"
rows="3"
class="form-input w-full resize-none font-mono text-sm"
placeholder="粘贴以 http://localhost:45462 开头的完整链接..."
></textarea>
/>
</div>
<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>
<i class="fas fa-check-circle text-green-500 mr-1" />
支持粘贴完整链接系统会自动提取授权码
</p>
<p class="text-xs text-gray-600">
<i class="fas fa-check-circle text-green-500 mr-1"></i>
<i class="fas fa-check-circle text-green-500 mr-1" />
也可以直接粘贴授权码code参数的值
</p>
</div>
@@ -234,18 +280,21 @@
<div class="flex gap-3 pt-4">
<button
type="button"
@click="$emit('back')"
class="flex-1 px-6 py-3 bg-gray-100 text-gray-700 rounded-xl font-semibold hover:bg-gray-200 transition-colors"
class="flex-1 px-6 py-3 bg-gray-100 text-gray-700 rounded-xl font-semibold hover:bg-gray-200 transition-colors"
@click="$emit('back')"
>
上一步
</button>
<button
type="button"
@click="exchangeCode"
:disabled="!canExchange || exchanging"
class="btn btn-primary flex-1 py-3 px-6 font-semibold"
@click="exchangeCode"
>
<div v-if="exchanging" class="loading-spinner mr-2"></div>
<div
v-if="exchanging"
class="loading-spinner mr-2"
/>
{{ exchanging ? '验证中...' : '完成授权' }}
</button>
</div>

View File

@@ -1,21 +1,26 @@
<template>
<div class="space-y-4">
<div class="flex items-center justify-between">
<h4 class="text-sm font-semibold text-gray-700">代理设置 (可选)</h4>
<h4 class="text-sm font-semibold text-gray-700">
代理设置 (可选)
</h4>
<label class="flex items-center cursor-pointer">
<input
type="checkbox"
v-model="proxy.enabled"
v-model="proxy.enabled"
type="checkbox"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
>
<span class="ml-2 text-sm text-gray-700">启用代理</span>
</label>
</div>
<div v-if="proxy.enabled" class="bg-gray-50 p-4 rounded-lg border border-gray-200 space-y-4">
<div
v-if="proxy.enabled"
class="bg-gray-50 p-4 rounded-lg border border-gray-200 space-y-4"
>
<div class="flex items-start gap-3 mb-3">
<div class="w-8 h-8 bg-gray-500 rounded-lg flex items-center justify-center flex-shrink-0">
<i class="fas fa-server text-white text-sm"></i>
<i class="fas fa-server text-white text-sm" />
</div>
<div class="flex-1">
<p class="text-sm text-gray-700">
@@ -33,9 +38,15 @@
v-model="proxy.type"
class="form-input w-full"
>
<option value="socks5">SOCKS5</option>
<option value="http">HTTP</option>
<option value="https">HTTPS</option>
<option value="socks5">
SOCKS5
</option>
<option value="http">
HTTP
</option>
<option value="https">
HTTPS
</option>
</select>
</div>
@@ -63,17 +74,23 @@
<div class="space-y-4">
<div class="flex items-center">
<input
type="checkbox"
id="proxyAuth"
v-model="showAuth"
id="proxyAuth"
type="checkbox"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
>
<label for="proxyAuth" class="ml-2 text-sm text-gray-700 cursor-pointer">
<label
for="proxyAuth"
class="ml-2 text-sm text-gray-700 cursor-pointer"
>
需要身份验证
</label>
</div>
<div v-if="showAuth" class="grid grid-cols-2 gap-4">
<div
v-if="showAuth"
class="grid grid-cols-2 gap-4"
>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">用户名</label>
<input
@@ -94,10 +111,10 @@
>
<button
type="button"
@click="showPassword = !showPassword"
class="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 hover:text-gray-600"
@click="showPassword = !showPassword"
>
<i :class="showPassword ? 'fas fa-eye-slash' : 'fas fa-eye'"></i>
<i :class="showPassword ? 'fas fa-eye-slash' : 'fas fa-eye'" />
</button>
</div>
</div>
@@ -106,7 +123,7 @@
<div class="bg-blue-50 p-3 rounded-lg border border-blue-200">
<p class="text-xs text-blue-700">
<i class="fas fa-info-circle mr-1"></i>
<i class="fas fa-info-circle mr-1" />
<strong>提示</strong>代理设置将用于所有与此账户相关的API请求请确保代理服务器支持HTTPS流量转发
</p>
</div>

View File

@@ -2,425 +2,550 @@
<Teleport to="body">
<div class="fixed inset-0 modal z-50 flex items-center justify-center p-4">
<div class="modal-content w-full max-w-4xl p-6 mx-auto max-h-[90vh] flex flex-col">
<div class="flex items-center justify-between mb-4">
<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 class="flex items-center justify-between mb-4">
<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" />
</div>
<h3 class="text-xl font-bold text-gray-900">
创建新的 API Key
</h3>
</div>
<h3 class="text-xl font-bold text-gray-900">创建新的 API Key</h3>
</div>
<button
@click="$emit('close')"
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-4 modal-scroll-content custom-scrollbar flex-1">
<div>
<label class="block text-sm font-semibold text-gray-700 mb-2">名称 <span class="text-red-500">*</span></label>
<input
v-model="form.name"
type="text"
required
class="form-input w-full"
:class="{ 'border-red-500': errors.name }"
placeholder="为您的 API Key 取一个名称"
@input="errors.name = ''"
<button
class="text-gray-400 hover:text-gray-600 transition-colors"
@click="$emit('close')"
>
<p v-if="errors.name" class="text-red-500 text-xs mt-1">{{ errors.name }}</p>
<i class="fas fa-times text-xl" />
</button>
</div>
<!-- 标签 -->
<div>
<label class="block text-sm font-semibold text-gray-700 mb-2">标签</label>
<div class="space-y-4">
<!-- 已选择的标签 -->
<div v-if="form.tags.length > 0">
<div class="text-xs font-medium text-gray-600 mb-2">已选择的标签:</div>
<div class="flex flex-wrap gap-2">
<span v-for="(tag, index) in form.tags" :key="'selected-' + index"
class="inline-flex items-center gap-1 px-3 py-1 bg-blue-100 text-blue-800 text-sm rounded-full">
{{ tag }}
<button type="button" @click="removeTag(index)"
class="ml-1 hover:text-blue-900">
<i class="fas fa-times text-xs"></i>
</button>
</span>
</div>
</div>
<!-- 可选择的已有标签 -->
<div v-if="unselectedTags.length > 0">
<div class="text-xs font-medium text-gray-600 mb-2">点击选择已有标签:</div>
<div class="flex flex-wrap gap-2">
<button
v-for="tag in unselectedTags"
:key="'available-' + tag"
type="button"
@click="selectTag(tag)"
class="inline-flex items-center gap-1 px-3 py-1 bg-gray-100 text-gray-700 text-sm rounded-full hover:bg-blue-100 hover:text-blue-700 transition-colors"
>
<i class="fas fa-tag text-gray-500 text-xs"></i>
{{ tag }}
</button>
</div>
</div>
<!-- 创建新标签 -->
<div>
<div class="text-xs font-medium text-gray-600 mb-2">创建新标签:</div>
<div class="flex gap-2">
<input
v-model="newTag"
type="text"
class="form-input flex-1"
placeholder="输入新标签名称"
@keypress.enter.prevent="addTag"
>
<button type="button" @click="addTag"
class="px-4 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors">
<i class="fas fa-plus"></i>
</button>
</div>
</div>
<p class="text-xs text-gray-500">用于标记不同团队或用途方便筛选管理</p>
</div>
</div>
<!-- 速率限制设置 -->
<div class="bg-blue-50 border border-blue-200 rounded-lg p-3">
<div class="flex items-center gap-2 mb-2">
<div class="w-6 h-6 bg-blue-500 rounded flex items-center justify-center flex-shrink-0">
<i class="fas fa-tachometer-alt text-white text-xs"></i>
</div>
<h4 class="font-semibold text-gray-800 text-sm">速率限制设置 (可选)</h4>
</div>
<div class="space-y-2">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-2">
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">时间窗口 (分钟)</label>
<input
v-model="form.rateLimitWindow"
type="number"
min="1"
placeholder="无限制"
class="form-input w-full text-sm"
>
<p class="text-xs text-gray-500 mt-0.5 ml-2">时间段单位</p>
</div>
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">请求次数限制</label>
<input
v-model="form.rateLimitRequests"
type="number"
min="1"
placeholder="无限制"
class="form-input w-full text-sm"
>
<p class="text-xs text-gray-500 mt-0.5 ml-2">窗口内最大请求</p>
</div>
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">Token 限制</label>
<input
v-model="form.tokenLimit"
type="number"
placeholder="无限制"
class="form-input w-full text-sm"
>
<p class="text-xs text-gray-500 mt-0.5 ml-2">窗口内最大Token</p>
</div>
</div>
<!-- 示例说明 -->
<div class="bg-blue-100 rounded-lg p-2">
<h5 class="text-xs font-semibold text-blue-800 mb-1">💡 使用示例</h5>
<div class="text-xs text-blue-700 space-y-0.5">
<div><strong>示例1:</strong> 时间窗口=60请求次数=1000 每60分钟最多1000次请求</div>
<div><strong>示例2:</strong> 时间窗口=1Token=10000 每分钟最多10,000个Token</div>
<div><strong>示例3:</strong> 窗口=30请求=50Token=100000 每30分钟50次请求且不超10万Token</div>
</div>
</div>
</div>
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-2">每日费用限制 (美元)</label>
<div class="space-y-2">
<div class="flex gap-2">
<button type="button" @click="form.dailyCostLimit = '50'" class="px-2 py-1 bg-gray-100 hover:bg-gray-200 rounded text-xs font-medium">$50</button>
<button type="button" @click="form.dailyCostLimit = '100'" class="px-2 py-1 bg-gray-100 hover:bg-gray-200 rounded text-xs font-medium">$100</button>
<button type="button" @click="form.dailyCostLimit = '200'" class="px-2 py-1 bg-gray-100 hover:bg-gray-200 rounded text-xs font-medium">$200</button>
<button type="button" @click="form.dailyCostLimit = ''" class="px-2 py-1 bg-gray-100 hover:bg-gray-200 rounded text-xs font-medium">自定义</button>
</div>
<form
class="space-y-4 modal-scroll-content custom-scrollbar flex-1"
@submit.prevent="createApiKey"
>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-2">名称 <span class="text-red-500">*</span></label>
<input
v-model="form.dailyCostLimit"
v-model="form.name"
type="text"
required
class="form-input w-full"
:class="{ 'border-red-500': errors.name }"
placeholder="为您的 API Key 取一个名称"
@input="errors.name = ''"
>
<p
v-if="errors.name"
class="text-red-500 text-xs mt-1"
>
{{ errors.name }}
</p>
</div>
<!-- 标签 -->
<div>
<label class="block text-sm font-semibold text-gray-700 mb-2">标签</label>
<div class="space-y-4">
<!-- 已选择的标签 -->
<div v-if="form.tags.length > 0">
<div class="text-xs font-medium text-gray-600 mb-2">
已选择的标签:
</div>
<div class="flex flex-wrap gap-2">
<span
v-for="(tag, index) in form.tags"
:key="'selected-' + index"
class="inline-flex items-center gap-1 px-3 py-1 bg-blue-100 text-blue-800 text-sm rounded-full"
>
{{ tag }}
<button
type="button"
class="ml-1 hover:text-blue-900"
@click="removeTag(index)"
>
<i class="fas fa-times text-xs" />
</button>
</span>
</div>
</div>
<!-- 可选择的已有标签 -->
<div v-if="unselectedTags.length > 0">
<div class="text-xs font-medium text-gray-600 mb-2">
点击选择已有标签:
</div>
<div class="flex flex-wrap gap-2">
<button
v-for="tag in unselectedTags"
:key="'available-' + tag"
type="button"
class="inline-flex items-center gap-1 px-3 py-1 bg-gray-100 text-gray-700 text-sm rounded-full hover:bg-blue-100 hover:text-blue-700 transition-colors"
@click="selectTag(tag)"
>
<i class="fas fa-tag text-gray-500 text-xs" />
{{ tag }}
</button>
</div>
</div>
<!-- 创建新标签 -->
<div>
<div class="text-xs font-medium text-gray-600 mb-2">
创建新标签:
</div>
<div class="flex gap-2">
<input
v-model="newTag"
type="text"
class="form-input flex-1"
placeholder="输入新标签名称"
@keypress.enter.prevent="addTag"
>
<button
type="button"
class="px-4 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors"
@click="addTag"
>
<i class="fas fa-plus" />
</button>
</div>
</div>
<p class="text-xs text-gray-500">
用于标记不同团队或用途方便筛选管理
</p>
</div>
</div>
<!-- 速率限制设置 -->
<div class="bg-blue-50 border border-blue-200 rounded-lg p-3">
<div class="flex items-center gap-2 mb-2">
<div class="w-6 h-6 bg-blue-500 rounded flex items-center justify-center flex-shrink-0">
<i class="fas fa-tachometer-alt text-white text-xs" />
</div>
<h4 class="font-semibold text-gray-800 text-sm">
速率限制设置 (可选)
</h4>
</div>
<div class="space-y-2">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-2">
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">时间窗口 (分钟)</label>
<input
v-model="form.rateLimitWindow"
type="number"
min="1"
placeholder="无限制"
class="form-input w-full text-sm"
>
<p class="text-xs text-gray-500 mt-0.5 ml-2">
时间段单位
</p>
</div>
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">请求次数限制</label>
<input
v-model="form.rateLimitRequests"
type="number"
min="1"
placeholder="无限制"
class="form-input w-full text-sm"
>
<p class="text-xs text-gray-500 mt-0.5 ml-2">
窗口内最大请求
</p>
</div>
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">Token 限制</label>
<input
v-model="form.tokenLimit"
type="number"
placeholder="无限制"
class="form-input w-full text-sm"
>
<p class="text-xs text-gray-500 mt-0.5 ml-2">
窗口内最大Token
</p>
</div>
</div>
<!-- 示例说明 -->
<div class="bg-blue-100 rounded-lg p-2">
<h5 class="text-xs font-semibold text-blue-800 mb-1">
💡 使用示例
</h5>
<div class="text-xs text-blue-700 space-y-0.5">
<div><strong>示例1:</strong> 时间窗口=60请求次数=1000 每60分钟最多1000次请求</div>
<div><strong>示例2:</strong> 时间窗口=1Token=10000 每分钟最多10,000个Token</div>
<div><strong>示例3:</strong> 窗口=30请求=50Token=100000 每30分钟50次请求且不超10万Token</div>
</div>
</div>
</div>
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-2">每日费用限制 (美元)</label>
<div class="space-y-2">
<div class="flex gap-2">
<button
type="button"
class="px-2 py-1 bg-gray-100 hover:bg-gray-200 rounded text-xs font-medium"
@click="form.dailyCostLimit = '50'"
>
$50
</button>
<button
type="button"
class="px-2 py-1 bg-gray-100 hover:bg-gray-200 rounded text-xs font-medium"
@click="form.dailyCostLimit = '100'"
>
$100
</button>
<button
type="button"
class="px-2 py-1 bg-gray-100 hover:bg-gray-200 rounded text-xs font-medium"
@click="form.dailyCostLimit = '200'"
>
$200
</button>
<button
type="button"
class="px-2 py-1 bg-gray-100 hover:bg-gray-200 rounded text-xs font-medium"
@click="form.dailyCostLimit = ''"
>
自定义
</button>
</div>
<input
v-model="form.dailyCostLimit"
type="number"
min="0"
step="0.01"
placeholder="0 表示无限制"
class="form-input w-full"
>
<p class="text-xs text-gray-500">
设置此 API Key 每日的费用限制超过限制将拒绝请求0 或留空表示无限制
</p>
</div>
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-2">并发限制 (可选)</label>
<input
v-model="form.concurrencyLimit"
type="number"
min="0"
step="0.01"
placeholder="0 表示无限制"
class="form-input w-full"
>
<p class="text-xs text-gray-500">设置此 API Key 每日的费用限制超过限制将拒绝请求0 或留空表示无限制</p>
<p class="text-xs text-gray-500 mt-2">
设置此 API Key 可同时处理的最大请求数0 或留空表示无限制
</p>
</div>
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-2">并发限制 (可选)</label>
<input
v-model="form.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-2">备注 (可选)</label>
<textarea
v-model="form.description"
rows="2"
class="form-input w-full resize-none text-sm"
placeholder="描述此 API Key 的用途..."
/>
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-2">备注 (可选)</label>
<textarea
v-model="form.description"
rows="2"
class="form-input w-full resize-none text-sm"
placeholder="描述此 API Key 的用途..."
></textarea>
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-2">有效期限</label>
<select
v-model="form.expireDuration"
@change="updateExpireAt"
class="form-input w-full"
>
<option value="">永不过期</option>
<option value="1d">1 </option>
<option value="7d">7 </option>
<option value="30d">30 </option>
<option value="90d">90 </option>
<option value="180d">180 </option>
<option value="365d">365 </option>
<option value="custom">自定义日期</option>
</select>
<div v-if="form.expireDuration === 'custom'" class="mt-3">
<input
v-model="form.customExpireDate"
type="datetime-local"
<div>
<label class="block text-sm font-semibold text-gray-700 mb-2">有效期限</label>
<select
v-model="form.expireDuration"
class="form-input w-full"
:min="minDateTime"
@change="updateCustomExpireAt"
@change="updateExpireAt"
>
<option value="">
永不过期
</option>
<option value="1d">
1
</option>
<option value="7d">
7
</option>
<option value="30d">
30
</option>
<option value="90d">
90
</option>
<option value="180d">
180
</option>
<option value="365d">
365
</option>
<option value="custom">
自定义日期
</option>
</select>
<div
v-if="form.expireDuration === 'custom'"
class="mt-3"
>
</div>
<p v-if="form.expiresAt" class="text-xs text-gray-500 mt-2">
将于 {{ formatExpireDate(form.expiresAt) }} 过期
</p>
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-2">服务权限</label>
<div class="flex gap-4">
<label class="flex items-center cursor-pointer">
<input
type="radio"
v-model="form.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="form.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="form.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-2">专属账号绑定 (可选)</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="form.claudeAccountId"
v-model="form.customExpireDate"
type="datetime-local"
class="form-input w-full"
:disabled="form.permissions === 'gemini'"
:min="minDateTime"
@change="updateCustomExpireAt"
>
<option value="">使用共享账号池</option>
<optgroup v-if="accounts.claude.filter(a => a.isDedicated && a.platform === 'claude-oauth').length > 0" label="Claude OAuth 账号">
</div>
<p
v-if="form.expiresAt"
class="text-xs text-gray-500 mt-2"
>
将于 {{ formatExpireDate(form.expiresAt) }} 过期
</p>
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-2">服务权限</label>
<div class="flex gap-4">
<label class="flex items-center cursor-pointer">
<input
v-model="form.permissions"
type="radio"
value="all"
class="mr-2"
>
<span class="text-sm text-gray-700">全部服务</span>
</label>
<label class="flex items-center cursor-pointer">
<input
v-model="form.permissions"
type="radio"
value="claude"
class="mr-2"
>
<span class="text-sm text-gray-700"> Claude</span>
</label>
<label class="flex items-center cursor-pointer">
<input
v-model="form.permissions"
type="radio"
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-2">专属账号绑定 (可选)</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="form.claudeAccountId"
class="form-input w-full"
:disabled="form.permissions === 'gemini'"
>
<option value="">
使用共享账号池
</option>
<optgroup v-if="accounts.claude.filter(a => a.isDedicated && a.platform === 'claude-oauth').length > 0" label="Claude OAuth 账号">
<option
v-for="account in accounts.claude.filter(a => a.isDedicated && a.platform === 'claude-oauth')"
:key="account.id"
:value="account.id"
>
{{ account.name }} ({{ account.status === 'active' ? '正常' : '异常' }})
</option>
</optgroup>
<optgroup v-if="accounts.claude.filter(a => a.isDedicated && a.platform === 'claude-console').length > 0" label="Claude Console 账号">
<option
v-for="account in accounts.claude.filter(a => a.isDedicated && a.platform === 'claude-console')"
:key="account.id"
:value="`console:${account.id}`"
>
{{ account.name }} ({{ account.status === 'active' ? '正常' : '异常' }})
</option>
</optgroup>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-600 mb-1">Gemini 专属账号</label>
<select
v-model="form.geminiAccountId"
class="form-input w-full"
:disabled="form.permissions === 'claude'"
>
<option value="">
使用共享账号池
</option>
<option
v-for="account in accounts.claude.filter(a => a.isDedicated && a.platform === 'claude-oauth')"
v-for="account in accounts.gemini.filter(a => a.isDedicated)"
:key="account.id"
:value="account.id"
>
{{ account.name }} ({{ account.status === 'active' ? '正常' : '异常' }})
</option>
</optgroup>
<optgroup v-if="accounts.claude.filter(a => a.isDedicated && a.platform === 'claude-console').length > 0" label="Claude Console 账号">
<option
v-for="account in accounts.claude.filter(a => a.isDedicated && a.platform === 'claude-console')"
:key="account.id"
:value="`console:${account.id}`"
>
{{ account.name }} ({{ account.status === 'active' ? '正常' : '异常' }})
</option>
</optgroup>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-600 mb-1">Gemini 专属账号</label>
<select
v-model="form.geminiAccountId"
class="form-input w-full"
:disabled="form.permissions === 'claude'"
>
<option value="">使用共享账号池</option>
<option
v-for="account in accounts.gemini.filter(a => a.isDedicated)"
:key="account.id"
:value="account.id"
>
{{ account.name }} ({{ account.status === 'active' ? '正常' : '异常' }})
</option>
</select>
</select>
</div>
</div>
<p class="text-xs text-gray-500 mt-2">
选择专属账号后此API Key将只使用该账号不选择则使用共享账号池
</p>
</div>
<p class="text-xs text-gray-500 mt-2">选择专属账号后此API Key将只使用该账号不选择则使用共享账号池</p>
</div>
<div>
<div class="flex items-center mb-2">
<input
type="checkbox"
v-model="form.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>
<div class="flex items-center mb-2">
<input
id="enableModelRestriction"
v-model="form.enableModelRestriction"
type="checkbox"
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="form.enableModelRestriction" class="space-y-2 bg-red-50 border border-red-200 rounded-lg p-3">
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">限制的模型列表</label>
<div class="flex flex-wrap gap-1 mb-2 min-h-[24px]">
<span
v-for="(model, index) in form.restrictedModels"
:key="index"
class="inline-flex items-center px-2 py-1 rounded-full text-xs bg-red-100 text-red-800"
>
{{ model }}
<div
v-if="form.enableModelRestriction"
class="space-y-2 bg-red-50 border border-red-200 rounded-lg p-3"
>
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">限制的模型列表</label>
<div class="flex flex-wrap gap-1 mb-2 min-h-[24px]">
<span
v-for="(model, index) in form.restrictedModels"
:key="index"
class="inline-flex items-center px-2 py-1 rounded-full text-xs bg-red-100 text-red-800"
>
{{ model }}
<button
type="button"
class="ml-1 text-red-600 hover:text-red-800"
@click="removeRestrictedModel(index)"
>
<i class="fas fa-times text-xs" />
</button>
</span>
<span
v-if="form.restrictedModels.length === 0"
class="text-gray-400 text-xs"
>
暂无限制的模型
</span>
</div>
<div class="flex gap-2">
<input
v-model="form.modelInput"
type="text"
placeholder="输入模型名称,按回车添加"
class="form-input flex-1 text-sm"
@keydown.enter.prevent="addRestrictedModel"
>
<button
type="button"
@click="removeRestrictedModel(index)"
class="ml-1 text-red-600 hover:text-red-800"
class="px-3 py-1.5 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors text-sm"
@click="addRestrictedModel"
>
<i class="fas fa-times text-xs"></i>
<i class="fas fa-plus" />
</button>
</span>
<span v-if="form.restrictedModels.length === 0" class="text-gray-400 text-xs">
暂无限制的模型
</span>
</div>
<p class="text-xs text-gray-500 mt-1">
例如claude-opus-4-20250514
</p>
</div>
<div class="flex gap-2">
<input
v-model="form.modelInput"
@keydown.enter.prevent="addRestrictedModel"
type="text"
placeholder="输入模型名称,按回车添加"
class="form-input flex-1 text-sm"
>
<button
type="button"
@click="addRestrictedModel"
class="px-3 py-1.5 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors text-sm"
>
<i class="fas fa-plus"></i>
</button>
</div>
<p class="text-xs text-gray-500 mt-1">例如claude-opus-4-20250514</p>
</div>
</div>
</div>
<!-- 客户端限制 -->
<div>
<div class="flex items-center mb-2">
<input
type="checkbox"
v-model="form.enableClientRestriction"
id="enableClientRestriction"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
>
<label for="enableClientRestriction" class="ml-2 text-sm font-semibold text-gray-700 cursor-pointer">
启用客户端限制
</label>
</div>
<!-- 客户端限制 -->
<div>
<div class="flex items-center mb-2">
<input
id="enableClientRestriction"
v-model="form.enableClientRestriction"
type="checkbox"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
>
<label
for="enableClientRestriction"
class="ml-2 text-sm font-semibold text-gray-700 cursor-pointer"
>
启用客户端限制
</label>
</div>
<div v-if="form.enableClientRestriction" class="bg-green-50 border border-green-200 rounded-lg p-3">
<div>
<label class="block text-xs font-medium text-gray-700 mb-2">允许的客户端</label>
<div class="space-y-1">
<div v-for="client in supportedClients" :key="client.id" class="flex items-start">
<input
type="checkbox"
:id="`client_${client.id}`"
:value="client.id"
v-model="form.allowedClients"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 mt-0.5"
<div
v-if="form.enableClientRestriction"
class="bg-green-50 border border-green-200 rounded-lg p-3"
>
<div>
<label class="block text-xs font-medium text-gray-700 mb-2">允许的客户端</label>
<div class="space-y-1">
<div
v-for="client in supportedClients"
:key="client.id"
class="flex items-start"
>
<label :for="`client_${client.id}`" class="ml-2 flex-1 cursor-pointer">
<span class="text-sm font-medium text-gray-700">{{ client.name }}</span>
<span class="text-xs text-gray-500 block">{{ client.description }}</span>
</label>
<input
:id="`client_${client.id}`"
v-model="form.allowedClients"
type="checkbox"
:value="client.id"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 mt-0.5"
>
<label
:for="`client_${client.id}`"
class="ml-2 flex-1 cursor-pointer"
>
<span class="text-sm font-medium text-gray-700">{{ client.name }}</span>
<span class="text-xs text-gray-500 block">{{ client.description }}</span>
</label>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="flex gap-3 pt-2">
<button
type="button"
@click="$emit('close')"
class="flex-1 px-4 py-2.5 bg-gray-100 text-gray-700 rounded-lg font-semibold hover:bg-gray-200 transition-colors text-sm"
>
取消
</button>
<button
type="submit"
:disabled="loading"
class="btn btn-primary flex-1 py-2.5 px-4 font-semibold text-sm"
>
<div v-if="loading" class="loading-spinner mr-2"></div>
<i v-else class="fas fa-plus mr-2"></i>
{{ loading ? '创建中...' : '创建' }}
</button>
</div>
</form>
<div class="flex gap-3 pt-2">
<button
type="button"
class="flex-1 px-4 py-2.5 bg-gray-100 text-gray-700 rounded-lg font-semibold hover:bg-gray-200 transition-colors text-sm"
@click="$emit('close')"
>
取消
</button>
<button
type="submit"
:disabled="loading"
class="btn btn-primary flex-1 py-2.5 px-4 font-semibold text-sm"
>
<div
v-if="loading"
class="loading-spinner mr-2"
/>
<i
v-else
class="fas fa-plus mr-2"
/>
{{ loading ? '创建中...' : '创建' }}
</button>
</div>
</form>
</div>
</div>
</div>
</Teleport>
</template>

View File

@@ -2,390 +2,491 @@
<Teleport to="body">
<div class="fixed inset-0 modal z-50 flex items-center justify-center p-4">
<div class="modal-content w-full max-w-4xl 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 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" />
</div>
<h3 class="text-xl font-bold text-gray-900">
编辑 API Key
</h3>
</div>
<h3 class="text-xl font-bold text-gray-900">编辑 API Key</h3>
</div>
<button
@click="$emit('close')"
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="form.name"
type="text"
disabled
class="form-input w-full bg-gray-100 cursor-not-allowed"
<button
class="text-gray-400 hover:text-gray-600 transition-colors"
@click="$emit('close')"
>
<p class="text-xs text-gray-500 mt-2">名称不可修改</p>
<i class="fas fa-times text-xl" />
</button>
</div>
<!-- 标签 -->
<div>
<label class="block text-sm font-semibold text-gray-700 mb-3">标签</label>
<div class="space-y-4">
<!-- 已选择的标签 -->
<div v-if="form.tags.length > 0">
<div class="text-xs font-medium text-gray-600 mb-2">已选择的标签:</div>
<div class="flex flex-wrap gap-2">
<span v-for="(tag, index) in form.tags" :key="'selected-' + index"
class="inline-flex items-center gap-1 px-3 py-1 bg-blue-100 text-blue-800 text-sm rounded-full">
{{ tag }}
<button type="button" @click="removeTag(index)"
class="ml-1 hover:text-blue-900">
<i class="fas fa-times text-xs"></i>
</button>
</span>
</div>
</div>
<!-- 可选择的已有标签 -->
<div v-if="unselectedTags.length > 0">
<div class="text-xs font-medium text-gray-600 mb-2">点击选择已有标签:</div>
<div class="flex flex-wrap gap-2">
<button
v-for="tag in unselectedTags"
:key="'available-' + tag"
type="button"
@click="selectTag(tag)"
class="inline-flex items-center gap-1 px-3 py-1 bg-gray-100 text-gray-700 text-sm rounded-full hover:bg-blue-100 hover:text-blue-700 transition-colors"
>
<i class="fas fa-tag text-gray-500 text-xs"></i>
{{ tag }}
</button>
</div>
</div>
<!-- 创建新标签 -->
<div>
<div class="text-xs font-medium text-gray-600 mb-2">创建新标签:</div>
<div class="flex gap-2">
<input
v-model="newTag"
type="text"
class="form-input flex-1"
placeholder="输入新标签名称"
@keypress.enter.prevent="addTag"
>
<button type="button" @click="addTag"
class="px-4 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors">
<i class="fas fa-plus"></i>
</button>
</div>
</div>
<p class="text-xs text-gray-500">用于标记不同团队或用途方便筛选管理</p>
</div>
</div>
<!-- 速率限制设置 -->
<div class="bg-blue-50 border border-blue-200 rounded-lg p-3">
<div class="flex items-center gap-2 mb-2">
<div class="w-6 h-6 bg-blue-500 rounded flex items-center justify-center flex-shrink-0">
<i class="fas fa-tachometer-alt text-white text-xs"></i>
</div>
<h4 class="font-semibold text-gray-800 text-sm">速率限制设置 (可选)</h4>
</div>
<div class="space-y-2">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-2">
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">时间窗口 (分钟)</label>
<input
v-model="form.rateLimitWindow"
type="number"
min="1"
placeholder="无限制"
class="form-input w-full text-sm"
>
<p class="text-xs text-gray-500 mt-0.5 ml-2">时间段单位</p>
</div>
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">请求次数限制</label>
<input
v-model="form.rateLimitRequests"
type="number"
min="1"
placeholder="无限制"
class="form-input w-full text-sm"
>
<p class="text-xs text-gray-500 mt-0.5 ml-2">窗口内最大请求</p>
</div>
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">Token 限制</label>
<input
v-model="form.tokenLimit"
type="number"
placeholder="无限制"
class="form-input w-full text-sm"
>
<p class="text-xs text-gray-500 mt-0.5 ml-2">窗口内最大Token</p>
</div>
</div>
<!-- 示例说明 -->
<div class="bg-blue-100 rounded-lg p-2">
<h5 class="text-xs font-semibold text-blue-800 mb-1">💡 使用示例</h5>
<div class="text-xs text-blue-700 space-y-0.5">
<div><strong>示例1:</strong> 时间窗口=60请求次数=1000 每60分钟最多1000次请求</div>
<div><strong>示例2:</strong> 时间窗口=1Token=10000 每分钟最多10,000个Token</div>
<div><strong>示例3:</strong> 窗口=30请求=50Token=100000 每30分钟50次请求且不超10万Token</div>
</div>
</div>
</div>
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-3">每日费用限制 (美元)</label>
<div class="space-y-3">
<div class="flex gap-2">
<button type="button" @click="form.dailyCostLimit = '50'" class="px-3 py-1 bg-gray-100 hover:bg-gray-200 rounded-lg text-sm font-medium">$50</button>
<button type="button" @click="form.dailyCostLimit = '100'" class="px-3 py-1 bg-gray-100 hover:bg-gray-200 rounded-lg text-sm font-medium">$100</button>
<button type="button" @click="form.dailyCostLimit = '200'" class="px-3 py-1 bg-gray-100 hover:bg-gray-200 rounded-lg text-sm font-medium">$200</button>
<button type="button" @click="form.dailyCostLimit = ''" class="px-3 py-1 bg-gray-100 hover:bg-gray-200 rounded-lg text-sm font-medium">自定义</button>
</div>
<form
class="space-y-6 modal-scroll-content custom-scrollbar flex-1"
@submit.prevent="updateApiKey"
>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-3">名称</label>
<input
v-model="form.dailyCostLimit"
:value="form.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>
<label class="block text-sm font-semibold text-gray-700 mb-3">标签</label>
<div class="space-y-4">
<!-- 已选择的标签 -->
<div v-if="form.tags.length > 0">
<div class="text-xs font-medium text-gray-600 mb-2">
已选择的标签:
</div>
<div class="flex flex-wrap gap-2">
<span
v-for="(tag, index) in form.tags"
:key="'selected-' + index"
class="inline-flex items-center gap-1 px-3 py-1 bg-blue-100 text-blue-800 text-sm rounded-full"
>
{{ tag }}
<button
type="button"
class="ml-1 hover:text-blue-900"
@click="removeTag(index)"
>
<i class="fas fa-times text-xs" />
</button>
</span>
</div>
</div>
<!-- 可选择的已有标签 -->
<div v-if="unselectedTags.length > 0">
<div class="text-xs font-medium text-gray-600 mb-2">
点击选择已有标签:
</div>
<div class="flex flex-wrap gap-2">
<button
v-for="tag in unselectedTags"
:key="'available-' + tag"
type="button"
class="inline-flex items-center gap-1 px-3 py-1 bg-gray-100 text-gray-700 text-sm rounded-full hover:bg-blue-100 hover:text-blue-700 transition-colors"
@click="selectTag(tag)"
>
<i class="fas fa-tag text-gray-500 text-xs" />
{{ tag }}
</button>
</div>
</div>
<!-- 创建新标签 -->
<div>
<div class="text-xs font-medium text-gray-600 mb-2">
创建新标签:
</div>
<div class="flex gap-2">
<input
v-model="newTag"
type="text"
class="form-input flex-1"
placeholder="输入新标签名称"
@keypress.enter.prevent="addTag"
>
<button
type="button"
class="px-4 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors"
@click="addTag"
>
<i class="fas fa-plus" />
</button>
</div>
</div>
<p class="text-xs text-gray-500">
用于标记不同团队或用途方便筛选管理
</p>
</div>
</div>
<!-- 速率限制设置 -->
<div class="bg-blue-50 border border-blue-200 rounded-lg p-3">
<div class="flex items-center gap-2 mb-2">
<div class="w-6 h-6 bg-blue-500 rounded flex items-center justify-center flex-shrink-0">
<i class="fas fa-tachometer-alt text-white text-xs" />
</div>
<h4 class="font-semibold text-gray-800 text-sm">
速率限制设置 (可选)
</h4>
</div>
<div class="space-y-2">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-2">
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">时间窗口 (分钟)</label>
<input
v-model="form.rateLimitWindow"
type="number"
min="1"
placeholder="无限制"
class="form-input w-full text-sm"
>
<p class="text-xs text-gray-500 mt-0.5 ml-2">
时间段单位
</p>
</div>
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">请求次数限制</label>
<input
v-model="form.rateLimitRequests"
type="number"
min="1"
placeholder="无限制"
class="form-input w-full text-sm"
>
<p class="text-xs text-gray-500 mt-0.5 ml-2">
窗口内最大请求
</p>
</div>
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">Token 限制</label>
<input
v-model="form.tokenLimit"
type="number"
placeholder="无限制"
class="form-input w-full text-sm"
>
<p class="text-xs text-gray-500 mt-0.5 ml-2">
窗口内最大Token
</p>
</div>
</div>
<!-- 示例说明 -->
<div class="bg-blue-100 rounded-lg p-2">
<h5 class="text-xs font-semibold text-blue-800 mb-1">
💡 使用示例
</h5>
<div class="text-xs text-blue-700 space-y-0.5">
<div><strong>示例1:</strong> 时间窗口=60请求次数=1000 每60分钟最多1000次请求</div>
<div><strong>示例2:</strong> 时间窗口=1Token=10000 每分钟最多10,000个Token</div>
<div><strong>示例3:</strong> 窗口=30请求=50Token=100000 每30分钟50次请求且不超10万Token</div>
</div>
</div>
</div>
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-3">每日费用限制 (美元)</label>
<div class="space-y-3">
<div class="flex gap-2">
<button
type="button"
class="px-3 py-1 bg-gray-100 hover:bg-gray-200 rounded-lg text-sm font-medium"
@click="form.dailyCostLimit = '50'"
>
$50
</button>
<button
type="button"
class="px-3 py-1 bg-gray-100 hover:bg-gray-200 rounded-lg text-sm font-medium"
@click="form.dailyCostLimit = '100'"
>
$100
</button>
<button
type="button"
class="px-3 py-1 bg-gray-100 hover:bg-gray-200 rounded-lg text-sm font-medium"
@click="form.dailyCostLimit = '200'"
>
$200
</button>
<button
type="button"
class="px-3 py-1 bg-gray-100 hover:bg-gray-200 rounded-lg text-sm font-medium"
@click="form.dailyCostLimit = ''"
>
自定义
</button>
</div>
<input
v-model="form.dailyCostLimit"
type="number"
min="0"
step="0.01"
placeholder="0 表示无限制"
class="form-input w-full"
>
<p class="text-xs text-gray-500">
设置此 API Key 每日的费用限制超过限制将拒绝请求0 或留空表示无限制
</p>
</div>
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-3">并发限制</label>
<input
v-model="form.concurrencyLimit"
type="number"
min="0"
step="0.01"
placeholder="0 表示无限制"
class="form-input w-full"
>
<p class="text-xs text-gray-500">设置此 API Key 每日的费用限制超过限制将拒绝请求0 或留空表示无限制</p>
<p class="text-xs text-gray-500 mt-2">
设置此 API Key 可同时处理的最大请求数
</p>
</div>
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-3">并发限制</label>
<input
v-model="form.concurrencyLimit"
type="number"
min="0"
placeholder="0 表示无限制"
class="form-input w-full"
>
<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="flex gap-4">
<label class="flex items-center cursor-pointer">
<input
type="radio"
v-model="form.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="form.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="form.permissions"
value="gemini"
class="mr-2"
>
<span class="text-sm text-gray-700"> Gemini</span>
</label>
<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
v-model="form.permissions"
type="radio"
value="all"
class="mr-2"
>
<span class="text-sm text-gray-700">全部服务</span>
</label>
<label class="flex items-center cursor-pointer">
<input
v-model="form.permissions"
type="radio"
value="claude"
class="mr-2"
>
<span class="text-sm text-gray-700"> Claude</span>
</label>
<label class="flex items-center cursor-pointer">
<input
v-model="form.permissions"
type="radio"
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>
<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="form.claudeAccountId"
class="form-input w-full"
:disabled="form.permissions === 'gemini'"
>
<option value="">使用共享账号池</option>
<optgroup v-if="accounts.claude.filter(a => a.isDedicated && a.platform === 'claude-oauth').length > 0" label="Claude OAuth 账号">
<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="form.claudeAccountId"
class="form-input w-full"
:disabled="form.permissions === 'gemini'"
>
<option value="">
使用共享账号池
</option>
<optgroup v-if="accounts.claude.filter(a => a.isDedicated && a.platform === 'claude-oauth').length > 0" label="Claude OAuth 账号">
<option
v-for="account in accounts.claude.filter(a => a.isDedicated && a.platform === 'claude-oauth')"
:key="account.id"
:value="account.id"
>
{{ account.name }} ({{ account.status === 'active' ? '正常' : '异常' }})
</option>
</optgroup>
<optgroup v-if="accounts.claude.filter(a => a.isDedicated && a.platform === 'claude-console').length > 0" label="Claude Console 账号">
<option
v-for="account in accounts.claude.filter(a => a.isDedicated && a.platform === 'claude-console')"
:key="account.id"
:value="`console:${account.id}`"
>
{{ account.name }} ({{ account.status === 'active' ? '正常' : '异常' }})
</option>
</optgroup>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-600 mb-1">Gemini 专属账号</label>
<select
v-model="form.geminiAccountId"
class="form-input w-full"
:disabled="form.permissions === 'claude'"
>
<option value="">
使用共享账号池
</option>
<option
v-for="account in accounts.claude.filter(a => a.isDedicated && a.platform === 'claude-oauth')"
v-for="account in accounts.gemini"
:key="account.id"
:value="account.id"
>
{{ account.name }} ({{ account.status === 'active' ? '正常' : '异常' }})
</option>
</optgroup>
<optgroup v-if="accounts.claude.filter(a => a.isDedicated && a.platform === 'claude-console').length > 0" label="Claude Console 账号">
<option
v-for="account in accounts.claude.filter(a => a.isDedicated && a.platform === 'claude-console')"
:key="account.id"
:value="`console:${account.id}`"
>
{{ account.name }} ({{ account.status === 'active' ? '正常' : '异常' }})
</option>
</optgroup>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-600 mb-1">Gemini 专属账号</label>
<select
v-model="form.geminiAccountId"
class="form-input w-full"
:disabled="form.permissions === 'claude'"
>
<option value="">使用共享账号池</option>
<option
v-for="account in accounts.gemini.filter(a => a.isDedicated)"
:key="account.id"
:value="account.id"
>
{{ account.name }} ({{ account.status === 'active' ? '正常' : '异常' }})
</option>
</select>
</select>
</div>
</div>
<p class="text-xs text-gray-500 mt-2">
修改绑定账号将影响此API Key的请求路由
</p>
</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="form.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>
<div class="flex items-center mb-3">
<input
id="editEnableModelRestriction"
v-model="form.enableModelRestriction"
type="checkbox"
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="form.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 form.restrictedModels"
:key="index"
class="inline-flex items-center px-3 py-1 rounded-full text-sm bg-red-100 text-red-800"
>
{{ model }}
<div
v-if="form.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 form.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"
class="ml-2 text-red-600 hover:text-red-800"
@click="removeRestrictedModel(index)"
>
<i class="fas fa-times text-xs" />
</button>
</span>
<span
v-if="form.restrictedModels.length === 0"
class="text-gray-400 text-sm"
>
暂无限制的模型
</span>
</div>
<div class="flex gap-2">
<input
v-model="form.modelInput"
type="text"
placeholder="输入模型名称,按回车添加"
class="form-input flex-1"
@keydown.enter.prevent="addRestrictedModel"
>
<button
type="button"
@click="removeRestrictedModel(index)"
class="ml-2 text-red-600 hover:text-red-800"
class="px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors"
@click="addRestrictedModel"
>
<i class="fas fa-times text-xs"></i>
<i class="fas fa-plus" />
</button>
</span>
<span v-if="form.restrictedModels.length === 0" class="text-gray-400 text-sm">
暂无限制的模型
</span>
</div>
<p class="text-xs text-gray-500 mt-2">
设置此API Key无法访问的模型例如claude-opus-4-20250514
</p>
</div>
<div class="flex gap-2">
<input
v-model="form.modelInput"
@keydown.enter.prevent="addRestrictedModel"
type="text"
placeholder="输入模型名称,按回车添加"
class="form-input flex-1"
>
<button
type="button"
@click="addRestrictedModel"
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>
<div class="flex items-center mb-3">
<input
type="checkbox"
v-model="form.enableClientRestriction"
id="editEnableClientRestriction"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
>
<label for="editEnableClientRestriction" class="ml-2 text-sm font-semibold text-gray-700 cursor-pointer">
启用客户端限制
</label>
</div>
<!-- 客户端限制 -->
<div>
<div class="flex items-center mb-3">
<input
id="editEnableClientRestriction"
v-model="form.enableClientRestriction"
type="checkbox"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
>
<label
for="editEnableClientRestriction"
class="ml-2 text-sm font-semibold text-gray-700 cursor-pointer"
>
启用客户端限制
</label>
</div>
<div v-if="form.enableClientRestriction" class="space-y-3">
<div>
<label class="block text-sm font-medium text-gray-600 mb-2">允许的客户端</label>
<p class="text-xs text-gray-500 mb-3">勾选允许使用此API Key的客户端</p>
<div class="space-y-2">
<div v-for="client in supportedClients" :key="client.id" class="flex items-start">
<input
type="checkbox"
:id="`edit_client_${client.id}`"
:value="client.id"
v-model="form.allowedClients"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 mt-0.5"
<div
v-if="form.enableClientRestriction"
class="space-y-3"
>
<div>
<label class="block text-sm font-medium text-gray-600 mb-2">允许的客户端</label>
<p class="text-xs text-gray-500 mb-3">
勾选允许使用此API Key的客户端
</p>
<div class="space-y-2">
<div
v-for="client in supportedClients"
:key="client.id"
class="flex items-start"
>
<label :for="`edit_client_${client.id}`" class="ml-2 flex-1 cursor-pointer">
<span class="text-sm font-medium text-gray-700">{{ client.name }}</span>
<span class="text-xs text-gray-500 block">{{ client.description }}</span>
</label>
<input
:id="`edit_client_${client.id}`"
v-model="form.allowedClients"
type="checkbox"
:value="client.id"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 mt-0.5"
>
<label
:for="`edit_client_${client.id}`"
class="ml-2 flex-1 cursor-pointer"
>
<span class="text-sm font-medium text-gray-700">{{ client.name }}</span>
<span class="text-xs text-gray-500 block">{{ client.description }}</span>
</label>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="flex gap-3 pt-4">
<button
type="button"
@click="$emit('close')"
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="loading"
class="btn btn-primary flex-1 py-3 px-6 font-semibold"
>
<div v-if="loading" class="loading-spinner mr-2"></div>
<i v-else class="fas fa-save mr-2"></i>
{{ loading ? '保存中...' : '保存修改' }}
</button>
</div>
</form>
<div class="flex gap-3 pt-4">
<button
type="button"
class="flex-1 px-6 py-3 bg-gray-100 text-gray-700 rounded-xl font-semibold hover:bg-gray-200 transition-colors"
@click="$emit('close')"
>
取消
</button>
<button
type="submit"
:disabled="loading"
class="btn btn-primary flex-1 py-3 px-6 font-semibold"
>
<div
v-if="loading"
class="loading-spinner mr-2"
/>
<i
v-else
class="fas fa-save mr-2"
/>
{{ loading ? '保存中...' : '保存修改' }}
</button>
</div>
</form>
</div>
</div>
</div>
</Teleport>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { showToast } from '@/utils/toast'
import { useAuthStore } from '@/stores/auth'
import { useClientsStore } from '@/stores/clients'
import { useApiKeysStore } from '@/stores/apiKeys'
import { apiClient } from '@/config/api'
@@ -403,7 +504,6 @@ const props = defineProps({
const emit = defineEmits(['close', 'success'])
const authStore = useAuthStore()
const clientsStore = useClientsStore()
const apiKeysStore = useApiKeysStore()
const loading = ref(false)
@@ -558,8 +658,9 @@ onMounted(async () => {
form.restrictedModels = props.apiKey.restrictedModels || []
form.allowedClients = props.apiKey.allowedClients || []
form.tags = props.apiKey.tags || []
form.enableModelRestriction = form.restrictedModels.length > 0
form.enableClientRestriction = form.allowedClients.length > 0
// 从后端数据中获取实际的启用状态,而不是根据数组长度推断
form.enableModelRestriction = props.apiKey.enableModelRestriction || false
form.enableClientRestriction = props.apiKey.enableClientRestriction || false
})
</script>

View File

@@ -2,97 +2,103 @@
<Teleport to="body">
<div 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 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" />
</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>
<div>
<h3 class="text-xl font-bold text-gray-900">API Key 创建成功</h3>
<p class="text-sm text-gray-600">请妥善保存您的 API Key</p>
<button
class="text-gray-400 hover:text-gray-600 transition-colors"
title="直接关闭(不推荐)"
@click="handleDirectClose"
>
<i class="fas fa-times text-xl" />
</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" />
</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>
<button
@click="handleDirectClose"
class="text-gray-400 hover:text-gray-600 transition-colors"
title="直接关闭(不推荐)"
>
<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>
<!-- 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">{{ apiKey.name }}</span>
</div>
</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请立即复制并妥善保存
<div v-if="apiKey.description">
<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">{{ apiKey.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
type="button"
class="btn-icon-sm hover:bg-gray-800 bg-gray-700"
:title="showFullKey ? '隐藏API Key' : '显示完整API Key'"
@click="toggleKeyVisibility"
>
<i :class="['fas', showFullKey ? 'fa-eye-slash' : 'fa-eye', 'text-gray-300']" />
</button>
</div>
</div>
<p class="text-xs text-gray-500 mt-2">
点击眼睛图标切换显示模式使用下方按钮复制完整 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">{{ apiKey.name }}</span>
</div>
<!-- 操作按钮 -->
<div class="flex gap-3">
<button
class="flex-1 btn btn-primary py-3 px-6 font-semibold flex items-center justify-center gap-2"
@click="copyApiKey"
>
<i class="fas fa-copy" />
复制 API Key
</button>
<button
class="px-6 py-3 bg-gray-200 text-gray-800 rounded-xl font-semibold hover:bg-gray-300 transition-colors border border-gray-300"
@click="handleClose"
>
我已保存
</button>
</div>
<div v-if="apiKey.description">
<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">{{ apiKey.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="toggleKeyVisibility"
type="button"
class="btn-icon-sm hover:bg-gray-800 bg-gray-700"
:title="showFullKey ? '隐藏API Key' : '显示完整API Key'"
>
<i :class="['fas', showFullKey ? 'fa-eye-slash' : 'fa-eye', 'text-gray-300']"></i>
</button>
</div>
</div>
<p class="text-xs text-gray-500 mt-2">
点击眼睛图标切换显示模式使用下方按钮复制完整 API Key
</p>
</div>
</div>
<!-- 操作按钮 -->
<div class="flex gap-3">
<button
@click="copyApiKey"
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="handleClose"
class="px-6 py-3 bg-gray-200 text-gray-800 rounded-xl font-semibold hover:bg-gray-300 transition-colors border border-gray-300"
>
我已保存
</button>
</div>
</div>
</div>
</Teleport>
</template>

View File

@@ -2,88 +2,120 @@
<Teleport to="body">
<div 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-green-500 to-green-600 rounded-xl flex items-center justify-center">
<i class="fas fa-clock text-white"></i>
</div>
<h3 class="text-xl font-bold text-gray-900">续期 API Key</h3>
</div>
<button
@click="$emit('close')"
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 modal-scroll-content custom-scrollbar flex-1">
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
<div class="flex items-start gap-3">
<div class="w-8 h-8 bg-blue-500 rounded-lg flex items-center justify-center flex-shrink-0">
<i class="fas fa-info text-white text-sm"></i>
</div>
<div>
<h4 class="font-semibold text-gray-800 mb-1">API Key 信息</h4>
<p class="text-sm text-gray-700">{{ apiKey.name }}</p>
<p class="text-xs text-gray-600 mt-1">
当前过期时间{{ apiKey.expiresAt ? formatExpireDate(apiKey.expiresAt) : '永不过期' }}
</p>
<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-clock text-white" />
</div>
<h3 class="text-xl font-bold text-gray-900">
续期 API Key
</h3>
</div>
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-3">续期时长</label>
<select
v-model="form.renewDuration"
@change="updateRenewExpireAt"
class="form-input w-full"
<button
class="text-gray-400 hover:text-gray-600 transition-colors"
@click="$emit('close')"
>
<option value="7d">延长 7 </option>
<option value="30d">延长 30 </option>
<option value="90d">延长 90 </option>
<option value="180d">延长 180 </option>
<option value="365d">延长 365 </option>
<option value="custom">自定义日期</option>
<option value="permanent">设为永不过期</option>
</select>
<div v-if="form.renewDuration === 'custom'" class="mt-3">
<input
v-model="form.customExpireDate"
type="datetime-local"
class="form-input w-full"
:min="minDateTime"
@change="updateCustomRenewExpireAt"
>
</div>
<p v-if="form.newExpiresAt" class="text-xs text-gray-500 mt-2">
新的过期时间{{ formatExpireDate(form.newExpiresAt) }}
</p>
<i class="fas fa-times text-xl" />
</button>
</div>
</div>
<div class="flex gap-3 pt-4">
<button
type="button"
@click="$emit('close')"
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="renewApiKey"
:disabled="loading || !form.renewDuration"
class="btn btn-primary flex-1 py-3 px-6 font-semibold"
>
<div v-if="loading" class="loading-spinner mr-2"></div>
<i v-else class="fas fa-clock mr-2"></i>
{{ loading ? '续期中...' : '确认续期' }}
</button>
<div class="space-y-6 modal-scroll-content custom-scrollbar flex-1">
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
<div class="flex items-start gap-3">
<div class="w-8 h-8 bg-blue-500 rounded-lg flex items-center justify-center flex-shrink-0">
<i class="fas fa-info text-white text-sm" />
</div>
<div>
<h4 class="font-semibold text-gray-800 mb-1">
API Key 信息
</h4>
<p class="text-sm text-gray-700">
{{ apiKey.name }}
</p>
<p class="text-xs text-gray-600 mt-1">
当前过期时间{{ apiKey.expiresAt ? formatExpireDate(apiKey.expiresAt) : '永不过期' }}
</p>
</div>
</div>
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-3">续期时长</label>
<select
v-model="form.renewDuration"
class="form-input w-full"
@change="updateRenewExpireAt"
>
<option value="7d">
延长 7
</option>
<option value="30d">
延长 30
</option>
<option value="90d">
延长 90
</option>
<option value="180d">
延长 180
</option>
<option value="365d">
延长 365
</option>
<option value="custom">
自定义日期
</option>
<option value="permanent">
设为永不过期
</option>
</select>
<div
v-if="form.renewDuration === 'custom'"
class="mt-3"
>
<input
v-model="form.customExpireDate"
type="datetime-local"
class="form-input w-full"
:min="minDateTime"
@change="updateCustomRenewExpireAt"
>
</div>
<p
v-if="form.newExpiresAt"
class="text-xs text-gray-500 mt-2"
>
新的过期时间{{ formatExpireDate(form.newExpiresAt) }}
</p>
</div>
</div>
<div class="flex gap-3 pt-4">
<button
type="button"
class="flex-1 px-6 py-3 bg-gray-100 text-gray-700 rounded-xl font-semibold hover:bg-gray-200 transition-colors"
@click="$emit('close')"
>
取消
</button>
<button
type="button"
:disabled="loading || !form.renewDuration"
class="btn btn-primary flex-1 py-3 px-6 font-semibold"
@click="renewApiKey"
>
<div
v-if="loading"
class="loading-spinner mr-2"
/>
<i
v-else
class="fas fa-clock mr-2"
/>
{{ loading ? '续期中...' : '确认续期' }}
</button>
</div>
</div>
</div>
</div>
</Teleport>
</template>

View File

@@ -3,10 +3,12 @@
<!-- 标题区域 -->
<div class="wide-card-title text-center mb-6">
<h2 class="text-2xl font-bold mb-2">
<i class="fas fa-chart-line mr-3"></i>
<i class="fas fa-chart-line mr-3" />
使用统计查询
</h2>
<p class="text-base text-gray-600">查询您的 API Key 使用情况和统计数据</p>
<p class="text-base text-gray-600">
查询您的 API Key 使用情况和统计数据
</p>
</div>
<!-- 输入区域 -->
@@ -15,7 +17,7 @@
<!-- API Key 输入 -->
<div class="lg:col-span-3">
<label class="block text-sm font-medium mb-2 text-gray-700">
<i class="fas fa-key mr-2"></i>
<i class="fas fa-key mr-2" />
输入您的 API Key
</label>
<input
@@ -23,8 +25,8 @@
type="password"
placeholder="请输入您的 API Key (cr_...)"
class="wide-card-input w-full"
@keyup.enter="queryStats"
:disabled="loading"
@keyup.enter="queryStats"
>
</div>
@@ -34,12 +36,18 @@
&nbsp;
</label>
<button
@click="queryStats"
:disabled="loading || !apiKey.trim()"
class="btn btn-primary btn-query w-full h-full flex items-center justify-center gap-2"
@click="queryStats"
>
<i v-if="loading" class="fas fa-spinner loading-spinner"></i>
<i v-else class="fas fa-search"></i>
<i
v-if="loading"
class="fas fa-spinner loading-spinner"
/>
<i
v-else
class="fas fa-search"
/>
{{ loading ? '查询中...' : '查询统计' }}
</button>
</div>
@@ -47,7 +55,7 @@
<!-- 安全提示 -->
<div class="security-notice mt-4">
<i class="fas fa-shield-alt mr-2"></i>
<i class="fas fa-shield-alt mr-2" />
您的 API Key 仅用于查询自己的统计数据不会被存储或用于其他用途
</div>
</div>

View File

@@ -3,7 +3,7 @@
<!-- 限制配置 -->
<div class="card p-6">
<h3 class="text-xl font-bold mb-4 flex items-center text-gray-900">
<i class="fas fa-shield-alt mr-3 text-red-500"></i>
<i class="fas fa-shield-alt mr-3 text-red-500" />
限制配置
</h3>
<div class="space-y-3">
@@ -19,8 +19,8 @@
<span class="text-gray-600">速率限制</span>
<span class="font-medium text-gray-900">
{{ statsData.limits.rateLimitRequests > 0 && statsData.limits.rateLimitWindow > 0
? `${statsData.limits.rateLimitRequests}次/${statsData.limits.rateLimitWindow}分钟`
: '无限制' }}
? `${statsData.limits.rateLimitRequests}次/${statsData.limits.rateLimitWindow}分钟`
: '无限制' }}
</span>
</div>
<div class="flex justify-between items-center">
@@ -30,13 +30,18 @@
<div class="flex justify-between items-center">
<span class="text-gray-600">模型限制</span>
<span class="font-medium text-gray-900">
<span v-if="statsData.restrictions.enableModelRestriction && statsData.restrictions.restrictedModels.length > 0"
class="text-orange-600">
<i class="fas fa-exclamation-triangle mr-1"></i>
<span
v-if="statsData.restrictions.enableModelRestriction && statsData.restrictions.restrictedModels.length > 0"
class="text-orange-600"
>
<i class="fas fa-exclamation-triangle mr-1" />
限制 {{ statsData.restrictions.restrictedModels.length }} 个模型
</span>
<span v-else class="text-green-600">
<i class="fas fa-check-circle mr-1"></i>
<span
v-else
class="text-green-600"
>
<i class="fas fa-check-circle mr-1" />
允许所有模型
</span>
</span>
@@ -44,13 +49,18 @@
<div class="flex justify-between items-center">
<span class="text-gray-600">客户端限制</span>
<span class="font-medium text-gray-900">
<span v-if="statsData.restrictions.enableClientRestriction && statsData.restrictions.allowedClients.length > 0"
class="text-orange-600">
<i class="fas fa-exclamation-triangle mr-1"></i>
<span
v-if="statsData.restrictions.enableClientRestriction && statsData.restrictions.allowedClients.length > 0"
class="text-orange-600"
>
<i class="fas fa-exclamation-triangle mr-1" />
限制 {{ statsData.restrictions.allowedClients.length }} 个客户端
</span>
<span v-else class="text-green-600">
<i class="fas fa-check-circle mr-1"></i>
<span
v-else
class="text-green-600"
>
<i class="fas fa-check-circle mr-1" />
允许所有客户端
</span>
</span>
@@ -59,53 +69,63 @@
</div>
<!-- 详细限制信息 -->
<div v-if="(statsData.restrictions.enableModelRestriction && statsData.restrictions.restrictedModels.length > 0) ||
(statsData.restrictions.enableClientRestriction && statsData.restrictions.allowedClients.length > 0)"
class="card p-6 mt-6">
<div
v-if="(statsData.restrictions.enableModelRestriction && statsData.restrictions.restrictedModels.length > 0) ||
(statsData.restrictions.enableClientRestriction && statsData.restrictions.allowedClients.length > 0)"
class="card p-6 mt-6"
>
<h3 class="text-xl font-bold mb-4 flex items-center text-gray-900">
<i class="fas fa-list-alt mr-3 text-amber-500"></i>
<i class="fas fa-list-alt mr-3 text-amber-500" />
详细限制信息
</h3>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- 模型限制详情 -->
<div v-if="statsData.restrictions.enableModelRestriction && statsData.restrictions.restrictedModels.length > 0"
class="bg-amber-50 border border-amber-200 rounded-lg p-4">
<div
v-if="statsData.restrictions.enableModelRestriction && statsData.restrictions.restrictedModels.length > 0"
class="bg-amber-50 border border-amber-200 rounded-lg p-4"
>
<h4 class="font-bold text-amber-800 mb-3 flex items-center">
<i class="fas fa-robot mr-2"></i>
<i class="fas fa-robot mr-2" />
受限模型列表
</h4>
<div class="space-y-2">
<div v-for="model in statsData.restrictions.restrictedModels"
:key="model"
class="bg-white rounded px-3 py-2 text-sm border border-amber-200">
<i class="fas fa-ban mr-2 text-red-500"></i>
<div
v-for="model in statsData.restrictions.restrictedModels"
:key="model"
class="bg-white rounded px-3 py-2 text-sm border border-amber-200"
>
<i class="fas fa-ban mr-2 text-red-500" />
<span class="text-gray-800">{{ model }}</span>
</div>
</div>
<p class="text-xs text-amber-700 mt-3">
<i class="fas fa-info-circle mr-1"></i>
<i class="fas fa-info-circle mr-1" />
此 API Key 不能访问以上列出的模型
</p>
</div>
<!-- 客户端限制详情 -->
<div v-if="statsData.restrictions.enableClientRestriction && statsData.restrictions.allowedClients.length > 0"
class="bg-blue-50 border border-blue-200 rounded-lg p-4">
<div
v-if="statsData.restrictions.enableClientRestriction && statsData.restrictions.allowedClients.length > 0"
class="bg-blue-50 border border-blue-200 rounded-lg p-4"
>
<h4 class="font-bold text-blue-800 mb-3 flex items-center">
<i class="fas fa-desktop mr-2"></i>
<i class="fas fa-desktop mr-2" />
允许的客户端
</h4>
<div class="space-y-2">
<div v-for="client in statsData.restrictions.allowedClients"
:key="client"
class="bg-white rounded px-3 py-2 text-sm border border-blue-200">
<i class="fas fa-check mr-2 text-green-500"></i>
<div
v-for="client in statsData.restrictions.allowedClients"
:key="client"
class="bg-white rounded px-3 py-2 text-sm border border-blue-200"
>
<i class="fas fa-check mr-2 text-green-500" />
<span class="text-gray-800">{{ client }}</span>
</div>
</div>
<p class="text-xs text-blue-700 mt-3">
<i class="fas fa-info-circle mr-1"></i>
<i class="fas fa-info-circle mr-1" />
API Key 只能被以上列出的客户端使用
</p>
</div>

View File

@@ -2,19 +2,27 @@
<div class="card p-6">
<div class="mb-6">
<h3 class="text-xl font-bold flex items-center text-gray-900">
<i class="fas fa-robot mr-3 text-indigo-500"></i>
<i class="fas fa-robot mr-3 text-indigo-500" />
模型使用统计 <span class="text-sm font-normal text-gray-600 ml-2">({{ statsPeriod === 'daily' ? '今日' : '本月' }})</span>
</h3>
</div>
<!-- 模型统计加载状态 -->
<div v-if="modelStatsLoading" class="text-center py-8">
<i class="fas fa-spinner loading-spinner text-2xl mb-2 text-gray-600"></i>
<p class="text-gray-600">加载模型统计数据中...</p>
<div
v-if="modelStatsLoading"
class="text-center py-8"
>
<i class="fas fa-spinner loading-spinner text-2xl mb-2 text-gray-600" />
<p class="text-gray-600">
加载模型统计数据中...
</p>
</div>
<!-- 模型统计数据 -->
<div v-else-if="modelStats.length > 0" class="space-y-4">
<div
v-else-if="modelStats.length > 0"
class="space-y-4"
>
<div
v-for="(model, index) in modelStats"
:key="index"
@@ -22,39 +30,66 @@
>
<div class="flex justify-between items-start mb-3">
<div>
<h4 class="font-bold text-lg text-gray-900">{{ model.model }}</h4>
<p class="text-gray-600 text-sm">{{ model.requests }} 次请求</p>
<h4 class="font-bold text-lg text-gray-900">
{{ model.model }}
</h4>
<p class="text-gray-600 text-sm">
{{ model.requests }} 次请求
</p>
</div>
<div class="text-right">
<div class="text-lg font-bold text-green-600">{{ model.formatted?.total || '$0.000000' }}</div>
<div class="text-sm text-gray-600">总费用</div>
<div class="text-lg font-bold text-green-600">
{{ model.formatted?.total || '$0.000000' }}
</div>
<div class="text-sm text-gray-600">
总费用
</div>
</div>
</div>
<div class="grid grid-cols-2 md:grid-cols-4 gap-3 text-sm">
<div class="bg-gray-50 rounded p-2">
<div class="text-gray-600">输入 Token</div>
<div class="font-medium text-gray-900">{{ formatNumber(model.inputTokens) }}</div>
<div class="text-gray-600">
输入 Token
</div>
<div class="font-medium text-gray-900">
{{ formatNumber(model.inputTokens) }}
</div>
</div>
<div class="bg-gray-50 rounded p-2">
<div class="text-gray-600">输出 Token</div>
<div class="font-medium text-gray-900">{{ formatNumber(model.outputTokens) }}</div>
<div class="text-gray-600">
输出 Token
</div>
<div class="font-medium text-gray-900">
{{ formatNumber(model.outputTokens) }}
</div>
</div>
<div class="bg-gray-50 rounded p-2">
<div class="text-gray-600">缓存创建</div>
<div class="font-medium text-gray-900">{{ formatNumber(model.cacheCreateTokens) }}</div>
<div class="text-gray-600">
缓存创建
</div>
<div class="font-medium text-gray-900">
{{ formatNumber(model.cacheCreateTokens) }}
</div>
</div>
<div class="bg-gray-50 rounded p-2">
<div class="text-gray-600">缓存读取</div>
<div class="font-medium text-gray-900">{{ formatNumber(model.cacheReadTokens) }}</div>
<div class="text-gray-600">
缓存读取
</div>
<div class="font-medium text-gray-900">
{{ formatNumber(model.cacheReadTokens) }}
</div>
</div>
</div>
</div>
</div>
<!-- 无模型数据 -->
<div v-else class="text-center py-8 text-gray-500">
<i class="fas fa-chart-pie text-3xl mb-3"></i>
<div
v-else
class="text-center py-8 text-gray-500"
>
<i class="fas fa-chart-pie text-3xl mb-3" />
<p>暂无{{ statsPeriod === 'daily' ? '今日' : '本月' }}模型使用数据</p>
</div>
</div>

View File

@@ -3,7 +3,7 @@
<!-- API Key 基本信息 -->
<div class="card p-6">
<h3 class="text-xl font-bold mb-4 flex items-center text-gray-900">
<i class="fas fa-info-circle mr-3 text-blue-500"></i>
<i class="fas fa-info-circle mr-3 text-blue-500" />
API Key 信息
</h3>
<div class="space-y-3">
@@ -13,8 +13,14 @@
</div>
<div class="flex justify-between items-center">
<span class="text-gray-600">状态</span>
<span :class="statsData.isActive ? 'text-green-600' : 'text-red-600'" class="font-medium">
<i :class="statsData.isActive ? 'fas fa-check-circle' : 'fas fa-times-circle'" class="mr-1"></i>
<span
:class="statsData.isActive ? 'text-green-600' : 'text-red-600'"
class="font-medium"
>
<i
:class="statsData.isActive ? 'fas fa-check-circle' : 'fas fa-times-circle'"
class="mr-1"
/>
{{ statsData.isActive ? '活跃' : '已停用' }}
</span>
</div>
@@ -29,20 +35,32 @@
<div class="flex justify-between items-center">
<span class="text-gray-600">过期时间</span>
<div v-if="statsData.expiresAt">
<div v-if="isApiKeyExpired(statsData.expiresAt)" class="text-red-600 font-medium">
<i class="fas fa-exclamation-circle mr-1"></i>
<div
v-if="isApiKeyExpired(statsData.expiresAt)"
class="text-red-600 font-medium"
>
<i class="fas fa-exclamation-circle mr-1" />
已过期
</div>
<div v-else-if="isApiKeyExpiringSoon(statsData.expiresAt)" class="text-orange-600 font-medium">
<i class="fas fa-clock mr-1"></i>
<div
v-else-if="isApiKeyExpiringSoon(statsData.expiresAt)"
class="text-orange-600 font-medium"
>
<i class="fas fa-clock mr-1" />
{{ formatExpireDate(statsData.expiresAt) }}
</div>
<div v-else class="text-gray-900 font-medium">
<div
v-else
class="text-gray-900 font-medium"
>
{{ formatExpireDate(statsData.expiresAt) }}
</div>
</div>
<div v-else class="text-gray-400 font-medium">
<i class="fas fa-infinity mr-1"></i>
<div
v-else
class="text-gray-400 font-medium"
>
<i class="fas fa-infinity mr-1" />
永不过期
</div>
</div>
@@ -52,25 +70,41 @@
<!-- 使用统计概览 -->
<div class="card p-6">
<h3 class="text-xl font-bold mb-4 flex items-center text-gray-900">
<i class="fas fa-chart-bar mr-3 text-green-500"></i>
<i class="fas fa-chart-bar mr-3 text-green-500" />
使用统计概览 <span class="text-sm font-normal text-gray-600 ml-2">({{ statsPeriod === 'daily' ? '今日' : '本月' }})</span>
</h3>
<div class="grid grid-cols-2 gap-4">
<div class="stat-card text-center">
<div class="text-3xl font-bold text-green-600">{{ formatNumber(currentPeriodData.requests) }}</div>
<div class="text-sm text-gray-600">{{ statsPeriod === 'daily' ? '今日' : '本月' }}请求数</div>
<div class="text-3xl font-bold text-green-600">
{{ formatNumber(currentPeriodData.requests) }}
</div>
<div class="text-sm text-gray-600">
{{ statsPeriod === 'daily' ? '今日' : '本月' }}请求数
</div>
</div>
<div class="stat-card text-center">
<div class="text-3xl font-bold text-blue-600">{{ formatNumber(currentPeriodData.allTokens) }}</div>
<div class="text-sm text-gray-600">{{ statsPeriod === 'daily' ? '今日' : '本月' }}Token</div>
<div class="text-3xl font-bold text-blue-600">
{{ formatNumber(currentPeriodData.allTokens) }}
</div>
<div class="text-sm text-gray-600">
{{ statsPeriod === 'daily' ? '今日' : '本月' }}Token数
</div>
</div>
<div class="stat-card text-center">
<div class="text-3xl font-bold text-purple-600">{{ currentPeriodData.formattedCost || '$0.000000' }}</div>
<div class="text-sm text-gray-600">{{ statsPeriod === 'daily' ? '今日' : '本月' }}费用</div>
<div class="text-3xl font-bold text-purple-600">
{{ currentPeriodData.formattedCost || '$0.000000' }}
</div>
<div class="text-sm text-gray-600">
{{ statsPeriod === 'daily' ? '今日' : '本月' }}费用
</div>
</div>
<div class="stat-card text-center">
<div class="text-3xl font-bold text-yellow-600">{{ formatNumber(currentPeriodData.inputTokens) }}</div>
<div class="text-sm text-gray-600">{{ statsPeriod === 'daily' ? '今日' : '本月' }}输入Token</div>
<div class="text-3xl font-bold text-yellow-600">
{{ formatNumber(currentPeriodData.inputTokens) }}
</div>
<div class="text-sm text-gray-600">
{{ statsPeriod === 'daily' ? '今日' : '本月' }}输入Token
</div>
</div>
</div>
</div>

View File

@@ -1,34 +1,34 @@
<template>
<div class="card p-6">
<h3 class="text-xl font-bold mb-4 flex items-center text-gray-900">
<i class="fas fa-coins mr-3 text-yellow-500"></i>
<i class="fas fa-coins mr-3 text-yellow-500" />
Token 使用分布 <span class="text-sm font-normal text-gray-600 ml-2">({{ statsPeriod === 'daily' ? '今日' : '本月' }})</span>
</h3>
<div class="space-y-3">
<div class="flex justify-between items-center">
<span class="text-gray-600 flex items-center">
<i class="fas fa-arrow-right mr-2 text-green-500"></i>
<i class="fas fa-arrow-right mr-2 text-green-500" />
输入 Token
</span>
<span class="font-medium text-gray-900">{{ formatNumber(currentPeriodData.inputTokens) }}</span>
</div>
<div class="flex justify-between items-center">
<span class="text-gray-600 flex items-center">
<i class="fas fa-arrow-left mr-2 text-blue-500"></i>
<i class="fas fa-arrow-left mr-2 text-blue-500" />
输出 Token
</span>
<span class="font-medium text-gray-900">{{ formatNumber(currentPeriodData.outputTokens) }}</span>
</div>
<div class="flex justify-between items-center">
<span class="text-gray-600 flex items-center">
<i class="fas fa-save mr-2 text-purple-500"></i>
<i class="fas fa-save mr-2 text-purple-500" />
缓存创建 Token
</span>
<span class="font-medium text-gray-900">{{ formatNumber(currentPeriodData.cacheCreateTokens) }}</span>
</div>
<div class="flex justify-between items-center">
<span class="text-gray-600 flex items-center">
<i class="fas fa-download mr-2 text-orange-500"></i>
<i class="fas fa-download mr-2 text-orange-500" />
缓存读取 Token
</span>
<span class="font-medium text-gray-900">{{ formatNumber(currentPeriodData.cacheReadTokens) }}</span>

View File

@@ -1,6 +1,9 @@
<template>
<Teleport to="body">
<Transition name="modal" appear>
<Transition
name="modal"
appear
>
<div
v-if="isVisible"
class="fixed inset-0 modal z-[100] flex items-center justify-center p-4"
@@ -9,29 +12,36 @@
<div class="modal-content w-full max-w-md p-6 mx-auto">
<div class="flex items-start gap-4 mb-6">
<div class="flex-shrink-0 w-12 h-12 bg-gradient-to-br from-amber-500 to-amber-600 rounded-xl flex items-center justify-center">
<i class="fas fa-exclamation-triangle text-white text-lg"></i>
<i class="fas fa-exclamation-triangle text-white text-lg" />
</div>
<div class="flex-1">
<h3 class="text-lg font-semibold text-gray-900 mb-2">{{ title }}</h3>
<div class="text-gray-600 leading-relaxed whitespace-pre-line">{{ message }}</div>
<h3 class="text-lg font-semibold text-gray-900 mb-2">
{{ title }}
</h3>
<div class="text-gray-600 leading-relaxed whitespace-pre-line">
{{ message }}
</div>
</div>
</div>
<div class="flex items-center justify-end gap-3">
<button
@click="handleCancel"
class="btn bg-gray-100 text-gray-700 hover:bg-gray-200 px-6 py-3"
:disabled="isProcessing"
@click="handleCancel"
>
{{ cancelText }}
</button>
<button
@click="handleConfirm"
class="btn btn-warning px-6 py-3"
:class="{ 'opacity-50 cursor-not-allowed': isProcessing }"
:disabled="isProcessing"
@click="handleConfirm"
>
<div v-if="isProcessing" class="loading-spinner mr-2"></div>
<div
v-if="isProcessing"
class="loading-spinner mr-2"
/>
{{ confirmText }}
</button>
</div>

View File

@@ -1,27 +1,34 @@
<template>
<Teleport to="body">
<div v-if="show" class="fixed inset-0 modal z-50 flex items-center justify-center p-4">
<div
v-if="show"
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>
<i class="fas fa-exclamation text-white text-xl" />
</div>
<div class="flex-1">
<h3 class="text-lg font-bold text-gray-900 mb-2">{{ title }}</h3>
<p class="text-gray-600 text-sm leading-relaxed whitespace-pre-line">{{ message }}</p>
<h3 class="text-lg font-bold text-gray-900 mb-2">
{{ title }}
</h3>
<p class="text-gray-600 text-sm leading-relaxed whitespace-pre-line">
{{ message }}
</p>
</div>
</div>
<div class="flex gap-3">
<button
@click="$emit('cancel')"
class="flex-1 px-4 py-2.5 bg-gray-100 text-gray-700 rounded-xl font-medium hover:bg-gray-200 transition-colors"
@click="$emit('cancel')"
>
{{ cancelText }}
</button>
<button
@click="$emit('confirm')"
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"
@click="$emit('confirm')"
>
{{ confirmText }}
</button>

View File

@@ -3,27 +3,45 @@
<!-- Logo区域 -->
<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 overflow-hidden">
<template v-if="!loading">
<img v-if="logoSrc"
:src="logoSrc"
alt="Logo"
class="w-8 h-8 object-contain"
@error="handleLogoError">
<i v-else class="fas fa-cloud text-xl text-gray-700"></i>
<img
v-if="logoSrc"
:src="logoSrc"
alt="Logo"
class="w-8 h-8 object-contain"
@error="handleLogoError"
>
<i
v-else
class="fas fa-cloud text-xl text-gray-700"
/>
</template>
<div v-else class="w-8 h-8 bg-gray-300/50 rounded animate-pulse"></div>
<div
v-else
class="w-8 h-8 bg-gray-300/50 rounded animate-pulse"
/>
</div>
<!-- 标题区域 -->
<div class="flex flex-col justify-center min-h-[48px]">
<div class="flex items-center gap-3">
<template v-if="!loading && title">
<h1 :class="['text-2xl font-bold header-title leading-tight', titleClass]">{{ title }}</h1>
<h1 :class="['text-2xl font-bold header-title leading-tight', titleClass]">
{{ title }}
</h1>
</template>
<div v-else-if="loading" class="h-8 w-64 bg-gray-300/50 rounded animate-pulse"></div>
<div
v-else-if="loading"
class="h-8 w-64 bg-gray-300/50 rounded animate-pulse"
/>
<!-- 插槽用于版本信息等额外内容 -->
<slot name="after-title"></slot>
<slot name="after-title" />
</div>
<p v-if="subtitle" class="text-gray-600 text-sm leading-tight mt-0.5">{{ subtitle }}</p>
<p
v-if="subtitle"
class="text-gray-600 text-sm leading-tight mt-0.5"
>
{{ subtitle }}
</p>
</div>
</div>
</template>

View File

@@ -2,12 +2,21 @@
<div class="stat-card">
<div class="flex items-start justify-between">
<div class="flex-1">
<p class="text-sm font-medium text-gray-600 mb-1">{{ title }}</p>
<p class="text-3xl font-bold text-gray-800">{{ value }}</p>
<p v-if="subtitle" class="text-sm text-gray-500 mt-2">{{ subtitle }}</p>
<p class="text-sm font-medium text-gray-600 mb-1">
{{ title }}
</p>
<p class="text-3xl font-bold text-gray-800">
{{ value }}
</p>
<p
v-if="subtitle"
class="text-sm text-gray-500 mt-2"
>
{{ subtitle }}
</p>
</div>
<div :class="['stat-icon', iconBgClass]">
<i :class="icon"></i>
<i :class="icon" />
</div>
</div>
</div>

View File

@@ -13,24 +13,31 @@
>
<div class="toast-content">
<div class="toast-icon">
<i :class="getIconClass(toast.type)"></i>
<i :class="getIconClass(toast.type)" />
</div>
<div class="toast-body">
<div v-if="toast.title" class="toast-title">{{ toast.title }}</div>
<div class="toast-message">{{ toast.message }}</div>
<div
v-if="toast.title"
class="toast-title"
>
{{ toast.title }}
</div>
<div class="toast-message">
{{ toast.message }}
</div>
</div>
<button
class="toast-close"
@click.stop="removeToast(toast.id)"
>
<i class="fas fa-times"></i>
<i class="fas fa-times" />
</button>
</div>
<div
v-if="toast.duration > 0"
class="toast-progress"
:style="{ animationDuration: `${toast.duration}ms` }"
></div>
/>
</div>
</div>
</Teleport>

View File

@@ -2,38 +2,65 @@
<div class="glass-strong rounded-3xl p-6">
<div class="flex flex-col md:flex-row justify-between items-start md:items-center mb-6 gap-4">
<h2 class="text-xl font-bold text-gray-800 flex items-center">
<i class="fas fa-robot mr-2 text-purple-500"></i>
<i class="fas fa-robot mr-2 text-purple-500" />
模型使用分布
</h2>
<el-radio-group v-model="modelPeriod" size="small" @change="handlePeriodChange">
<el-radio-button label="daily">今日</el-radio-button>
<el-radio-button label="total">累计</el-radio-button>
<el-radio-group
v-model="modelPeriod"
size="small"
@change="handlePeriodChange"
>
<el-radio-button label="daily">
今日
</el-radio-button>
<el-radio-button label="total">
累计
</el-radio-button>
</el-radio-group>
</div>
<div v-if="dashboardStore.dashboardModelStats.length === 0" class="text-center py-12 text-gray-500">
<i class="fas fa-chart-pie text-4xl mb-3 opacity-30"></i>
<div
v-if="dashboardStore.dashboardModelStats.length === 0"
class="text-center py-12 text-gray-500"
>
<i class="fas fa-chart-pie text-4xl mb-3 opacity-30" />
<p>暂无模型使用数据</p>
</div>
<div v-else class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div
v-else
class="grid grid-cols-1 lg:grid-cols-2 gap-6"
>
<!-- 饼图 -->
<div class="relative" style="height: 300px;">
<canvas ref="chartCanvas"></canvas>
<div
class="relative"
style="height: 300px;"
>
<canvas ref="chartCanvas" />
</div>
<!-- 数据列表 -->
<div class="space-y-3">
<div v-for="(stat, index) in sortedStats" :key="stat.model"
class="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<div
v-for="(stat, index) in sortedStats"
:key="stat.model"
class="flex items-center justify-between p-3 bg-gray-50 rounded-lg"
>
<div class="flex items-center gap-3">
<div class="w-4 h-4 rounded" :style="`background-color: ${getColor(index)}`"></div>
<div
class="w-4 h-4 rounded"
:style="`background-color: ${getColor(index)}`"
/>
<span class="font-medium text-gray-700">{{ stat.model }}</span>
</div>
<div class="text-right">
<p class="font-semibold text-gray-800">{{ formatNumber(stat.requests) }} 请求</p>
<p class="text-sm text-gray-500">{{ formatNumber(stat.totalTokens) }} tokens</p>
<p class="font-semibold text-gray-800">
{{ formatNumber(stat.requests) }} 请求
</p>
<p class="text-sm text-gray-500">
{{ formatNumber(stat.totalTokens) }} tokens
</p>
</div>
</div>
</div>

View File

@@ -2,24 +2,45 @@
<div class="glass-strong rounded-3xl p-6 mb-8">
<div class="flex flex-col md:flex-row justify-between items-start md:items-center mb-6 gap-4">
<h2 class="text-xl font-bold text-gray-800 flex items-center">
<i class="fas fa-chart-area mr-2 text-blue-500"></i>
<i class="fas fa-chart-area mr-2 text-blue-500" />
使用趋势
</h2>
<div class="flex items-center gap-3">
<el-radio-group v-model="granularity" size="small" @change="handleGranularityChange">
<el-radio-button label="day">按天</el-radio-button>
<el-radio-button label="hour">按小时</el-radio-button>
<el-radio-group
v-model="granularity"
size="small"
@change="handleGranularityChange"
>
<el-radio-button label="day">
按天
</el-radio-button>
<el-radio-button label="hour">
按小时
</el-radio-button>
</el-radio-group>
<el-select v-model="trendPeriod" size="small" style="width: 120px" @change="handlePeriodChange">
<el-option :label="`最近${period.days}天`" :value="period.days" v-for="period in periodOptions" :key="period.days" />
<el-select
v-model="trendPeriod"
size="small"
style="width: 120px"
@change="handlePeriodChange"
>
<el-option
v-for="period in periodOptions"
:key="period.days"
:label="`最近${period.days}天`"
:value="period.days"
/>
</el-select>
</div>
</div>
<div class="relative" style="height: 300px;">
<canvas ref="chartCanvas"></canvas>
<div
class="relative"
style="height: 300px;"
>
<canvas ref="chartCanvas" />
</div>
</div>
</template>

View File

@@ -1,6 +1,9 @@
<template>
<!-- 顶部导航 -->
<div class="glass-strong rounded-3xl p-6 mb-8 shadow-xl" style="z-index: 10; position: relative;">
<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">
<LogoTitle
@@ -22,7 +25,7 @@
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>
<i class="fas fa-arrow-up text-[10px]" />
<span>新版本</span>
</a>
</div>
@@ -32,12 +35,15 @@
<!-- 用户菜单 -->
<div class="relative user-menu-container">
<button
@click="userMenuOpen = !userMenuOpen"
class="btn btn-primary px-4 py-3 flex items-center gap-2 relative"
@click="userMenuOpen = !userMenuOpen"
>
<i class="fas fa-user-circle"></i>
<i class="fas fa-user-circle" />
<span>{{ currentUser.username || 'Admin' }}</span>
<i class="fas fa-chevron-down text-xs transition-transform duration-200" :class="{ 'rotate-180': userMenuOpen }"></i>
<i
class="fas fa-chevron-down text-xs transition-transform duration-200"
:class="{ 'rotate-180': userMenuOpen }"
/>
</button>
<!-- 悬浮菜单 -->
@@ -53,10 +59,13 @@
<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
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>有新版本
<i class="fas fa-arrow-up mr-1" />有新版本
</span>
<span class="font-mono text-green-600">v{{ versionInfo.latest }}</span>
</div>
@@ -65,47 +74,60 @@
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>查看更新
<i class="fas fa-external-link-alt mr-1" />查看更新
</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
v-else-if="versionInfo.checkingUpdate"
class="mt-2 text-center text-xs text-gray-500"
>
<i class="fas fa-spinner fa-spin mr-1" />检查更新中...
</div>
<div v-else class="mt-2 text-center">
<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">
<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>当前已是最新版本
<i class="fas fa-check-circle mr-1" />当前已是最新版本
</p>
</div>
<button
v-else
key="button"
@click="checkForUpdates()"
class="text-xs text-blue-500 hover:text-blue-700 transition-colors"
@click="checkForUpdates()"
>
<i class="fas fa-sync-alt mr-1"></i>检查更新
<i class="fas fa-sync-alt mr-1" />检查更新
</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"
@click="openChangePasswordModal"
>
<i class="fas fa-key text-blue-500"></i>
<i class="fas fa-key text-blue-500" />
<span>修改账户信息</span>
</button>
<hr class="my-2 border-gray-200">
<button
@click="logout"
class="w-full px-4 py-3 text-left text-gray-700 hover:bg-gray-50 transition-colors flex items-center gap-3"
@click="logout"
>
<i class="fas fa-sign-out-alt text-red-500"></i>
<i class="fas fa-sign-out-alt text-red-500" />
<span>退出登录</span>
</button>
</div>
@@ -114,24 +136,32 @@
</div>
<!-- 修改账户信息模态框 -->
<div v-if="showChangePasswordModal" class="fixed inset-0 modal z-50 flex items-center justify-center p-4">
<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>
<i class="fas fa-key text-white" />
</div>
<h3 class="text-xl font-bold text-gray-900">修改账户信息</h3>
<h3 class="text-xl font-bold text-gray-900">
修改账户信息
</h3>
</div>
<button
@click="closeChangePasswordModal"
class="text-gray-400 hover:text-gray-600 transition-colors"
@click="closeChangePasswordModal"
>
<i class="fas fa-times text-xl"></i>
<i class="fas fa-times text-xl" />
</button>
</div>
<form @submit.prevent="changePassword" class="space-y-6 modal-scroll-content custom-scrollbar flex-1">
<form
class="space-y-6 modal-scroll-content custom-scrollbar flex-1"
@submit.prevent="changePassword"
>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-3">当前用户名</label>
<input
@@ -140,7 +170,9 @@
disabled
class="form-input w-full bg-gray-100 cursor-not-allowed"
>
<p class="text-xs text-gray-500 mt-2">当前用户名输入新用户名以修改</p>
<p class="text-xs text-gray-500 mt-2">
当前用户名输入新用户名以修改
</p>
</div>
<div>
@@ -151,7 +183,9 @@
class="form-input w-full"
placeholder="输入新用户名(留空保持不变)"
>
<p class="text-xs text-gray-500 mt-2">留空表示不修改用户名</p>
<p class="text-xs text-gray-500 mt-2">
留空表示不修改用户名
</p>
</div>
<div>
@@ -174,7 +208,9 @@
class="form-input w-full"
placeholder="请输入新密码"
>
<p class="text-xs text-gray-500 mt-2">密码长度至少8位</p>
<p class="text-xs text-gray-500 mt-2">
密码长度至少8位
</p>
</div>
<div>
@@ -191,8 +227,8 @@
<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"
class="flex-1 px-6 py-3 bg-gray-100 text-gray-700 rounded-xl font-semibold hover:bg-gray-200 transition-colors"
@click="closeChangePasswordModal"
>
取消
</button>
@@ -201,8 +237,14 @@
: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>
<div
v-if="changePasswordLoading"
class="loading-spinner mr-2"
/>
<i
v-else
class="fas fa-save mr-2"
/>
{{ changePasswordLoading ? '保存中...' : '保存修改' }}
</button>
</div>

View File

@@ -4,14 +4,23 @@
<AppHeader />
<!-- 主内容区域 -->
<div class="glass-strong rounded-3xl p-6 shadow-xl" style="z-index: 1; min-height: calc(100vh - 240px);">
<div
class="glass-strong rounded-3xl p-6 shadow-xl"
style="z-index: 1; min-height: calc(100vh - 240px);"
>
<!-- 标签栏 -->
<TabBar :active-tab="activeTab" @tab-change="handleTabChange" />
<TabBar
:active-tab="activeTab"
@tab-change="handleTabChange"
/>
<!-- 内容区域 -->
<div class="tab-content">
<router-view v-slot="{ Component }">
<transition name="slide-up" mode="out-in">
<transition
name="slide-up"
mode="out-in"
>
<keep-alive :include="['DashboardView', 'ApiKeysView']">
<component :is="Component" />
</keep-alive>

View File

@@ -3,13 +3,13 @@
<button
v-for="tab in tabs"
:key="tab.key"
@click="$emit('tab-change', 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'
]"
@click="$emit('tab-change', tab.key)"
>
<i :class="tab.icon + ' mr-2'"></i>{{ tab.name }}
<i :class="tab.icon + ' mr-2'" />{{ tab.name }}
</button>
</div>
</template>