优化仪表盘自动刷新UI布局

- 调整Element Plus日期选择器宽度为400px,确保时间完整显示
- 重新设计自动刷新控制的样式和布局
- 统一控制栏所有元素的高度,保持视觉一致性
- 使用更精致的开关组件和优化的交互效果

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
shaw
2025-07-30 15:36:52 +08:00
parent 1ca753c79a
commit 7116a6e043
29 changed files with 3869 additions and 2344 deletions

View File

@@ -1,36 +1,48 @@
<template> <template>
<Teleport to="body"> <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="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 justify-between mb-6">
<div class="flex items-center gap-3"> <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"> <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> </div>
<h3 class="text-xl font-bold text-gray-900">{{ isEdit ? '编辑账户' : '添加账户' }}</h3> <h3 class="text-xl font-bold text-gray-900">
{{ isEdit ? '编辑账户' : '添加账户' }}
</h3>
</div> </div>
<button <button
@click="$emit('close')"
class="text-gray-400 hover:text-gray-600 transition-colors" 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> </button>
</div> </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 space-x-4">
<div class="flex items-center"> <div class="flex items-center">
<div :class="['w-8 h-8 rounded-full flex items-center justify-center text-sm font-semibold', <div
oauthStep >= 1 ? 'bg-blue-500 text-white' : 'bg-gray-200 text-gray-500']"> :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 1
</div> </div>
<span class="ml-2 text-sm font-medium text-gray-700">基本信息</span> <span class="ml-2 text-sm font-medium text-gray-700">基本信息</span>
</div> </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="flex items-center">
<div :class="['w-8 h-8 rounded-full flex items-center justify-center text-sm font-semibold', <div
oauthStep >= 2 ? 'bg-blue-500 text-white' : 'bg-gray-200 text-gray-500']"> :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 2
</div> </div>
<span class="ml-2 text-sm font-medium text-gray-700">授权认证</span> <span class="ml-2 text-sm font-medium text-gray-700">授权认证</span>
@@ -46,8 +58,8 @@
<div class="flex gap-4"> <div class="flex gap-4">
<label class="flex items-center cursor-pointer"> <label class="flex items-center cursor-pointer">
<input <input
type="radio"
v-model="form.platform" v-model="form.platform"
type="radio"
value="claude" value="claude"
class="mr-2" class="mr-2"
> >
@@ -55,8 +67,8 @@
</label> </label>
<label class="flex items-center cursor-pointer"> <label class="flex items-center cursor-pointer">
<input <input
type="radio"
v-model="form.platform" v-model="form.platform"
type="radio"
value="claude-console" value="claude-console"
class="mr-2" class="mr-2"
> >
@@ -64,8 +76,8 @@
</label> </label>
<label class="flex items-center cursor-pointer"> <label class="flex items-center cursor-pointer">
<input <input
type="radio"
v-model="form.platform" v-model="form.platform"
type="radio"
value="gemini" value="gemini"
class="mr-2" class="mr-2"
> >
@@ -79,8 +91,8 @@
<div class="flex gap-4"> <div class="flex gap-4">
<label class="flex items-center cursor-pointer"> <label class="flex items-center cursor-pointer">
<input <input
type="radio"
v-model="form.addType" v-model="form.addType"
type="radio"
value="oauth" value="oauth"
class="mr-2" class="mr-2"
> >
@@ -88,8 +100,8 @@
</label> </label>
<label class="flex items-center cursor-pointer"> <label class="flex items-center cursor-pointer">
<input <input
type="radio"
v-model="form.addType" v-model="form.addType"
type="radio"
value="manual" value="manual"
class="mr-2" class="mr-2"
> >
@@ -108,7 +120,12 @@
:class="{ 'border-red-500': errors.name }" :class="{ 'border-red-500': errors.name }"
placeholder="为账户设置一个易识别的名称" 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>
<div> <div>
@@ -118,7 +135,7 @@
rows="3" rows="3"
class="form-input w-full resize-none" class="form-input w-full resize-none"
placeholder="账户用途说明..." placeholder="账户用途说明..."
></textarea> />
</div> </div>
<div> <div>
@@ -126,8 +143,8 @@
<div class="flex gap-4"> <div class="flex gap-4">
<label class="flex items-center cursor-pointer"> <label class="flex items-center cursor-pointer">
<input <input
type="radio"
v-model="form.accountType" v-model="form.accountType"
type="radio"
value="shared" value="shared"
class="mr-2" class="mr-2"
> >
@@ -135,8 +152,8 @@
</label> </label>
<label class="flex items-center cursor-pointer"> <label class="flex items-center cursor-pointer">
<input <input
type="radio"
v-model="form.accountType" v-model="form.accountType"
type="radio"
value="dedicated" value="dedicated"
class="mr-2" class="mr-2"
> >
@@ -159,26 +176,43 @@
> >
<div class="mt-2 p-3 bg-yellow-50 border border-yellow-200 rounded-lg"> <div class="mt-2 p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
<div class="flex items-start gap-2"> <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"> <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> <p>某些 Google 账号特别是绑定了 Google Cloud 的账号会被识别为 Workspace 账号需要提供额外的项目编号</p>
<div class="mt-2 p-2 bg-white rounded border border-yellow-300"> <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"> <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>复制<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> </ol>
</div> </div>
<p class="mt-2"><strong>提示</strong>如果您的账号是普通个人账号未绑定 Google Cloud请留空此字段</p> <p class="mt-2">
<strong>提示</strong>如果您的账号是普通个人账号未绑定 Google Cloud请留空此字段
</p>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Claude Console 特定字段 --> <!-- 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> <div>
<label class="block text-sm font-semibold text-gray-700 mb-3">API URL *</label> <label class="block text-sm font-semibold text-gray-700 mb-3">API URL *</label>
<input <input
@@ -189,7 +223,12 @@
:class="{ 'border-red-500': errors.apiUrl }" :class="{ 'border-red-500': errors.apiUrl }"
placeholder="例如https://api.example.com" 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>
<div> <div>
@@ -202,7 +241,12 @@
:class="{ 'border-red-500': errors.apiKey }" :class="{ 'border-red-500': errors.apiKey }"
placeholder="请输入API Key" 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>
<div> <div>
@@ -210,22 +254,22 @@
<div class="mb-2 flex gap-2"> <div class="mb-2 flex gap-2">
<button <button
type="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" 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 + claude-sonnet-4-20250514
</button> </button>
<button <button
type="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" 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 + claude-opus-4-20250514
</button> </button>
<button <button
type="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" 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 + claude-3-5-haiku-20241022
</button> </button>
@@ -235,8 +279,10 @@
rows="3" rows="3"
class="form-input w-full resize-none" class="form-input w-full resize-none"
placeholder="每行一个模型,留空表示支持所有模型。特别注意,ClaudeCode必须加上hiku模型" 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>
<div> <div>
@@ -258,7 +304,9 @@
class="form-input w-full" class="form-input w-full"
placeholder="默认60分钟" 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>
</div> </div>
@@ -273,37 +321,58 @@
class="form-input w-full" class="form-input w-full"
placeholder="数字越小优先级越高默认50" 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> </div>
<!-- 手动输入 Token 字段 --> <!-- 手动输入 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="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"> <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>
<div> <div>
<h5 class="font-semibold text-blue-900 mb-2">手动输入 Token</h5> <h5 class="font-semibold text-blue-900 mb-2">
<p v-if="form.platform === 'claude'" class="text-sm text-blue-800 mb-2"> 手动输入 Token
</h5>
<p
v-if="form.platform === 'claude'"
class="text-sm text-blue-800 mb-2"
>
请输入有效的 Claude Access Token如果您有 Refresh Token建议也一并填写以支持自动刷新 请输入有效的 Claude Access Token如果您有 Refresh Token建议也一并填写以支持自动刷新
</p> </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建议也一并填写以支持自动刷新 请输入有效的 Gemini Access Token如果您有 Refresh Token建议也一并填写以支持自动刷新
</p> </p>
<div class="bg-white/80 rounded-lg p-3 mt-2 mb-2 border border-blue-300"> <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"> <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 的方法 获取 Access Token 的方法
</p> </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 Code 的机器上获取 <code class="bg-blue-100 px-1 py-0.5 rounded font-mono">~/.claude/.credentials.json</code> 文件中的凭证
请勿使用 Claude 官网 API Keys 页面的密钥 请勿使用 Claude 官网 API Keys 页面的密钥
</p> </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> 请从已登录 Gemini CLI 的机器上获取 <code class="bg-blue-100 px-1 py-0.5 rounded font-mono">~/.config/gemini/credentials.json</code>
</p> </p>
</div> </div>
<p class="text-xs text-blue-600">💡 如果未填写 Refresh TokenToken 过期后需要手动更新</p> <p class="text-xs text-blue-600">
💡 如果未填写 Refresh TokenToken 过期后需要手动更新
</p>
</div> </div>
</div> </div>
@@ -316,8 +385,13 @@
class="form-input w-full resize-none font-mono text-xs" class="form-input w-full resize-none font-mono text-xs"
:class="{ 'border-red-500': errors.accessToken }" :class="{ 'border-red-500': errors.accessToken }"
placeholder="请输入 Access Token..." 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>
<div> <div>
@@ -327,7 +401,7 @@
rows="4" rows="4"
class="form-input w-full resize-none font-mono text-xs" class="form-input w-full resize-none font-mono text-xs"
placeholder="请输入 Refresh Token..." placeholder="请输入 Refresh Token..."
></textarea> />
</div> </div>
</div> </div>
@@ -337,28 +411,31 @@
<div class="flex gap-3 pt-4"> <div class="flex gap-3 pt-4">
<button <button
type="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>
<button <button
v-if="form.addType === 'oauth' && form.platform !== 'claude-console'" v-if="form.addType === 'oauth' && form.platform !== 'claude-console'"
type="button" type="button"
@click="nextStep"
:disabled="loading" :disabled="loading"
class="btn btn-primary flex-1 py-3 px-6 font-semibold" class="btn btn-primary flex-1 py-3 px-6 font-semibold"
@click="nextStep"
> >
下一步 下一步
</button> </button>
<button <button
v-else v-else
type="button" type="button"
@click="createAccount"
:disabled="loading" :disabled="loading"
class="btn btn-primary flex-1 py-3 px-6 font-semibold" 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 ? '创建中...' : '创建' }} {{ loading ? '创建中...' : '创建' }}
</button> </button>
</div> </div>
@@ -375,7 +452,10 @@
/> />
<!-- 编辑模式 --> <!-- 编辑模式 -->
<div v-if="isEdit" class="space-y-6"> <div
v-if="isEdit"
class="space-y-6"
>
<!-- 基本信息 --> <!-- 基本信息 -->
<div> <div>
<label class="block text-sm font-semibold text-gray-700 mb-3">账户名称</label> <label class="block text-sm font-semibold text-gray-700 mb-3">账户名称</label>
@@ -395,7 +475,7 @@
rows="3" rows="3"
class="form-input w-full resize-none" class="form-input w-full resize-none"
placeholder="账户用途说明..." placeholder="账户用途说明..."
></textarea> />
</div> </div>
<div> <div>
@@ -403,8 +483,8 @@
<div class="flex gap-4"> <div class="flex gap-4">
<label class="flex items-center cursor-pointer"> <label class="flex items-center cursor-pointer">
<input <input
type="radio"
v-model="form.accountType" v-model="form.accountType"
type="radio"
value="shared" value="shared"
class="mr-2" class="mr-2"
> >
@@ -412,8 +492,8 @@
</label> </label>
<label class="flex items-center cursor-pointer"> <label class="flex items-center cursor-pointer">
<input <input
type="radio"
v-model="form.accountType" v-model="form.accountType"
type="radio"
value="dedicated" value="dedicated"
class="mr-2" class="mr-2"
> >
@@ -450,11 +530,16 @@
class="form-input w-full" class="form-input w-full"
placeholder="数字越小优先级越高" 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> </div>
<!-- Claude Console 特定字段编辑模式--> <!-- 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> <div>
<label class="block text-sm font-semibold text-gray-700 mb-3">API URL</label> <label class="block text-sm font-semibold text-gray-700 mb-3">API URL</label>
<input <input
@@ -474,7 +559,9 @@
class="form-input w-full" class="form-input w-full"
placeholder="留空表示不更新" 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>
<div> <div>
@@ -482,32 +569,32 @@
<div class="mb-2 flex gap-2"> <div class="mb-2 flex gap-2">
<button <button
type="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" 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 + claude-sonnet-4-20250514
</button> </button>
<button <button
type="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" 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 + claude-opus-4-20250514
</button> </button>
<button <button
type="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"
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 + claude-3-5-haiku-20241022
</button> </button>
</div> </div>
<textarea <textarea
v-model="form.supportedModels" v-model="form.supportedModels"
rows="3" rows="3"
class="form-input w-full resize-none" class="form-input w-full resize-none"
placeholder="每行一个模型,留空表示支持所有模型。特别注意,ClaudeCode必须加上hiku模型" placeholder="每行一个模型,留空表示支持所有模型。特别注意,ClaudeCode必须加上hiku模型"
></textarea> />
</div> </div>
<div> <div>
@@ -532,15 +619,24 @@
</div> </div>
<!-- Token 更新 --> <!-- 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="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"> <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>
<div> <div>
<h5 class="font-semibold text-amber-900 mb-2">更新 Token</h5> <h5 class="font-semibold text-amber-900 mb-2">
<p class="text-sm text-amber-800 mb-2">可以更新 Access Token Refresh Token为了安全起见不会显示当前的 Token </p> 更新 Token
<p class="text-xs text-amber-600">💡 留空表示不更新该字段</p> </h5>
<p class="text-sm text-amber-800 mb-2">
可以更新 Access Token Refresh Token为了安全起见不会显示当前的 Token
</p>
<p class="text-xs text-amber-600">
💡 留空表示不更新该字段
</p>
</div> </div>
</div> </div>
@@ -552,7 +648,7 @@
rows="4" rows="4"
class="form-input w-full resize-none font-mono text-xs" class="form-input w-full resize-none font-mono text-xs"
placeholder="留空表示不更新..." placeholder="留空表示不更新..."
></textarea> />
</div> </div>
<div> <div>
@@ -562,7 +658,7 @@
rows="4" rows="4"
class="form-input w-full resize-none font-mono text-xs" class="form-input w-full resize-none font-mono text-xs"
placeholder="留空表示不更新..." placeholder="留空表示不更新..."
></textarea> />
</div> </div>
</div> </div>
</div> </div>
@@ -573,18 +669,21 @@
<div class="flex gap-3 pt-4"> <div class="flex gap-3 pt-4">
<button <button
type="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>
<button <button
type="button" type="button"
@click="updateAccount"
:disabled="loading" :disabled="loading"
class="btn btn-primary flex-1 py-3 px-6 font-semibold" 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 ? '更新中...' : '更新' }} {{ loading ? '更新中...' : '更新' }}
</button> </button>
</div> </div>

View File

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

View File

@@ -1,21 +1,26 @@
<template> <template>
<div class="space-y-4"> <div class="space-y-4">
<div class="flex items-center justify-between"> <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"> <label class="flex items-center cursor-pointer">
<input <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" 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> <span class="ml-2 text-sm text-gray-700">启用代理</span>
</label> </label>
</div> </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="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"> <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>
<div class="flex-1"> <div class="flex-1">
<p class="text-sm text-gray-700"> <p class="text-sm text-gray-700">
@@ -33,9 +38,15 @@
v-model="proxy.type" v-model="proxy.type"
class="form-input w-full" class="form-input w-full"
> >
<option value="socks5">SOCKS5</option> <option value="socks5">
<option value="http">HTTP</option> SOCKS5
<option value="https">HTTPS</option> </option>
<option value="http">
HTTP
</option>
<option value="https">
HTTPS
</option>
</select> </select>
</div> </div>
@@ -63,17 +74,23 @@
<div class="space-y-4"> <div class="space-y-4">
<div class="flex items-center"> <div class="flex items-center">
<input <input
type="checkbox" id="proxyAuth"
v-model="showAuth" 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" 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> </label>
</div> </div>
<div v-if="showAuth" class="grid grid-cols-2 gap-4"> <div
v-if="showAuth"
class="grid grid-cols-2 gap-4"
>
<div> <div>
<label class="block text-sm font-medium text-gray-700 mb-2">用户名</label> <label class="block text-sm font-medium text-gray-700 mb-2">用户名</label>
<input <input
@@ -94,10 +111,10 @@
> >
<button <button
type="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" 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> </button>
</div> </div>
</div> </div>
@@ -106,7 +123,7 @@
<div class="bg-blue-50 p-3 rounded-lg border border-blue-200"> <div class="bg-blue-50 p-3 rounded-lg border border-blue-200">
<p class="text-xs text-blue-700"> <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流量转发 <strong>提示</strong>代理设置将用于所有与此账户相关的API请求请确保代理服务器支持HTTPS流量转发
</p> </p>
</div> </div>

View File

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

View File

@@ -2,372 +2,474 @@
<Teleport to="body"> <Teleport to="body">
<div class="fixed inset-0 modal z-50 flex items-center justify-center p-4"> <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="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 justify-between mb-6">
<div class="flex items-center gap-3"> <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"> <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> <i class="fas fa-edit text-white" />
</div>
<h3 class="text-xl font-bold text-gray-900">
编辑 API Key
</h3>
</div> </div>
<h3 class="text-xl font-bold text-gray-900">编辑 API Key</h3> <button
class="text-gray-400 hover:text-gray-600 transition-colors"
@click="$emit('close')"
>
<i class="fas fa-times text-xl" />
</button>
</div> </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"> <form
<div> class="space-y-6 modal-scroll-content custom-scrollbar flex-1"
<label class="block text-sm font-semibold text-gray-700 mb-3">名称</label> @submit.prevent="updateApiKey"
<input >
:value="form.name" <div>
type="text" <label class="block text-sm font-semibold text-gray-700 mb-3">名称</label>
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" @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>
<input <input
v-model="form.dailyCostLimit" :value="form.name"
type="number" type="text"
min="0" disabled
step="0.01" class="form-input w-full bg-gray-100 cursor-not-allowed"
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">
名称不可修改
</p>
</div> </div>
</div>
<div> <!-- 标签 -->
<label class="block text-sm font-semibold text-gray-700 mb-3">并发限制</label> <div>
<input <label class="block text-sm font-semibold text-gray-700 mb-3">标签</label>
v-model="form.concurrencyLimit" <div class="space-y-4">
type="number" <!-- 已选择的标签 -->
min="0" <div v-if="form.tags.length > 0">
placeholder="0 表示无限制" <div class="text-xs font-medium text-gray-600 mb-2">
class="form-input w-full" 已选择的标签:
> </div>
<p class="text-xs text-gray-500 mt-2">设置此 API Key 可同时处理的最大请求数</p> <div class="flex flex-wrap gap-2">
</div> <span
v-for="(tag, index) in form.tags"
<div> :key="'selected-' + index"
<label class="block text-sm font-semibold text-gray-700 mb-3">服务权限</label> class="inline-flex items-center gap-1 px-3 py-1 bg-blue-100 text-blue-800 text-sm rounded-full"
<div class="flex gap-4"> >
<label class="flex items-center cursor-pointer"> {{ tag }}
<input <button
type="radio" type="button"
v-model="form.permissions" class="ml-1 hover:text-blue-900"
value="all" @click="removeTag(index)"
class="mr-2" >
> <i class="fas fa-times text-xs" />
<span class="text-sm text-gray-700">全部服务</span> </button>
</label> </span>
<label class="flex items-center cursor-pointer"> </div>
<input </div>
type="radio"
v-model="form.permissions" <!-- 可选择的已有标签 -->
value="claude" <div v-if="unselectedTags.length > 0">
class="mr-2" <div class="text-xs font-medium text-gray-600 mb-2">
> 点击选择已有标签:
<span class="text-sm text-gray-700"> Claude</span> </div>
</label> <div class="flex flex-wrap gap-2">
<label class="flex items-center cursor-pointer"> <button
<input v-for="tag in unselectedTags"
type="radio" :key="'available-' + tag"
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-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>
<option
v-for="account in accounts.claude"
:key="account.id"
:value="account.id"
>
{{ account.name }} ({{ account.status === 'active' ? '正常' : '异常' }})
</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-600 mb-1">Gemini 专属账号</label>
<select
v-model="form.geminiAccountId"
class="form-input w-full"
:disabled="form.permissions === 'claude'"
>
<option value="">使用共享账号池</option>
<option
v-for="account in accounts.gemini"
:key="account.id"
:value="account.id"
>
{{ account.name }} ({{ account.status === 'active' ? '正常' : '异常' }})
</option>
</select>
</div>
</div>
<p class="text-xs text-gray-500 mt-2">修改绑定账号将影响此API Key的请求路由</p>
</div>
<div>
<div class="flex items-center mb-3">
<input
type="checkbox"
v-model="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 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" type="button"
@click="removeRestrictedModel(index)" 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"
class="ml-2 text-red-600 hover:text-red-800" @click="selectTag(tag)"
> >
<i class="fas fa-times text-xs"></i> <i class="fas fa-tag text-gray-500 text-xs" />
{{ tag }}
</button> </button>
</span> </div>
<span v-if="form.restrictedModels.length === 0" class="text-gray-400 text-sm">
暂无限制的模型
</span>
</div> </div>
<div class="flex gap-2">
<input <!-- 创建新标签 -->
v-model="form.modelInput" <div>
@keydown.enter.prevent="addRestrictedModel" <div class="text-xs font-medium text-gray-600 mb-2">
type="text" 创建新标签:
placeholder="输入模型名称,按回车添加" </div>
class="form-input flex-1" <div class="flex gap-2">
> <input
<button v-model="newTag"
type="button" type="text"
@click="addRestrictedModel" class="form-input flex-1"
class="px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors" placeholder="输入新标签名称"
> @keypress.enter.prevent="addTag"
<i class="fas fa-plus"></i> >
</button> <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> </div>
<p class="text-xs text-gray-500 mt-2">设置此API Key无法访问的模型例如claude-opus-4-20250514</p>
<p class="text-xs text-gray-500">
用于标记不同团队或用途方便筛选管理
</p>
</div> </div>
</div> </div>
</div>
<!-- 客户端限制 --> <!-- 速率限制设置 -->
<div> <div class="bg-blue-50 border border-blue-200 rounded-lg p-3">
<div class="flex items-center mb-3"> <div class="flex items-center gap-2 mb-2">
<input <div class="w-6 h-6 bg-blue-500 rounded flex items-center justify-center flex-shrink-0">
type="checkbox" <i class="fas fa-tachometer-alt text-white text-xs" />
v-model="form.enableClientRestriction" </div>
id="editEnableClientRestriction" <h4 class="font-semibold text-gray-800 text-sm">
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500" 速率限制设置 (可选)
> </h4>
<label for="editEnableClientRestriction" class="ml-2 text-sm font-semibold text-gray-700 cursor-pointer"> </div>
启用客户端限制
</label>
</div>
<div v-if="form.enableClientRestriction" class="space-y-3"> <div class="space-y-2">
<div> <div class="grid grid-cols-1 lg:grid-cols-3 gap-2">
<label class="block text-sm font-medium text-gray-600 mb-2">允许的客户端</label> <div>
<p class="text-xs text-gray-500 mb-3">勾选允许使用此API Key的客户端</p> <label class="block text-xs font-medium text-gray-700 mb-1">时间窗口 (分钟)</label>
<div class="space-y-2">
<div v-for="client in supportedClients" :key="client.id" class="flex items-start">
<input <input
type="checkbox" v-model="form.rateLimitWindow"
:id="`edit_client_${client.id}`" type="number"
:value="client.id" min="1"
v-model="form.allowedClients" placeholder="无限制"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 mt-0.5" class="form-input w-full text-sm"
> >
<label :for="`edit_client_${client.id}`" class="ml-2 flex-1 cursor-pointer"> <p class="text-xs text-gray-500 mt-0.5 ml-2">
<span class="text-sm font-medium text-gray-700">{{ client.name }}</span> 时间段单位
<span class="text-xs text-gray-500 block">{{ client.description }}</span> </p>
</label> </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> </div>
</div> </div>
</div>
<div class="flex gap-3 pt-4"> <div>
<button <label class="block text-sm font-semibold text-gray-700 mb-3">每日费用限制 (美元)</label>
type="button" <div class="space-y-3">
@click="$emit('close')" <div class="flex gap-2">
class="flex-1 px-6 py-3 bg-gray-100 text-gray-700 rounded-xl font-semibold hover:bg-gray-200 transition-colors" <button
> type="button"
取消 class="px-3 py-1 bg-gray-100 hover:bg-gray-200 rounded-lg text-sm font-medium"
</button> @click="form.dailyCostLimit = '50'"
<button >
type="submit" $50
:disabled="loading" </button>
class="btn btn-primary flex-1 py-3 px-6 font-semibold" <button
> type="button"
<div v-if="loading" class="loading-spinner mr-2"></div> class="px-3 py-1 bg-gray-100 hover:bg-gray-200 rounded-lg text-sm font-medium"
<i v-else class="fas fa-save mr-2"></i> @click="form.dailyCostLimit = '100'"
{{ loading ? '保存中...' : '保存修改' }} >
</button> $100
</div> </button>
</form> <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"
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
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-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>
<option
v-for="account in accounts.claude"
:key="account.id"
:value="account.id"
>
{{ account.name }} ({{ account.status === 'active' ? '正常' : '异常' }})
</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-600 mb-1">Gemini 专属账号</label>
<select
v-model="form.geminiAccountId"
class="form-input w-full"
:disabled="form.permissions === 'claude'"
>
<option value="">
使用共享账号池
</option>
<option
v-for="account in accounts.gemini"
:key="account.id"
:value="account.id"
>
{{ account.name }} ({{ account.status === 'active' ? '正常' : '异常' }})
</option>
</select>
</div>
</div>
<p class="text-xs text-gray-500 mt-2">
修改绑定账号将影响此API Key的请求路由
</p>
</div>
<div>
<div class="flex items-center mb-3">
<input
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 }}
<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"
class="px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors"
@click="addRestrictedModel"
>
<i class="fas fa-plus" />
</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
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
: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 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>
</div>
</Teleport> </Teleport>
</template> </template>

View File

@@ -2,97 +2,103 @@
<Teleport to="body"> <Teleport to="body">
<div class="fixed inset-0 modal z-50 flex items-center justify-center p-4"> <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="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 justify-between mb-6">
<div class="flex items-center gap-3"> <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"> <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> <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>
<div> <button
<h3 class="text-xl font-bold text-gray-900">API Key 创建成功</h3> class="text-gray-400 hover:text-gray-600 transition-colors"
<p class="text-sm text-gray-600">请妥善保存您的 API Key</p> 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>
</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>
<!-- 警告提示 --> <!-- API Key 信息 -->
<div class="bg-amber-50 border-l-4 border-amber-400 p-4 mb-6"> <div class="space-y-4 mb-6">
<div class="flex items-start"> <div>
<div class="w-6 h-6 bg-amber-400 rounded-lg flex items-center justify-center flex-shrink-0 mt-0.5"> <label class="block text-sm font-semibold text-gray-700 mb-2">API Key 名称</label>
<i class="fas fa-exclamation-triangle text-white text-sm"></i> <div class="p-3 bg-gray-50 rounded-lg border">
<span class="text-gray-900 font-medium">{{ apiKey.name }}</span>
</div>
</div> </div>
<div class="ml-3">
<h5 class="font-semibold text-amber-900 mb-1">重要提醒</h5> <div v-if="apiKey.description">
<p class="text-sm text-amber-800"> <label class="block text-sm font-semibold text-gray-700 mb-2">备注</label>
这是您唯一能看到完整 API Key 的机会关闭此窗口后系统将不再显示完整的 API Key请立即复制并妥善保存 <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> </p>
</div> </div>
</div> </div>
</div>
<!-- API Key 信息 --> <!-- 操作按钮 -->
<div class="space-y-4 mb-6"> <div class="flex gap-3">
<div> <button
<label class="block text-sm font-semibold text-gray-700 mb-2">API Key 名称</label> class="flex-1 btn btn-primary py-3 px-6 font-semibold flex items-center justify-center gap-2"
<div class="p-3 bg-gray-50 rounded-lg border"> @click="copyApiKey"
<span class="text-gray-900 font-medium">{{ apiKey.name }}</span> >
</div> <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>
<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> </div>
</div>
</Teleport> </Teleport>
</template> </template>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,34 +1,34 @@
<template> <template>
<div class="card p-6"> <div class="card p-6">
<h3 class="text-xl font-bold mb-4 flex items-center text-gray-900"> <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> Token 使用分布 <span class="text-sm font-normal text-gray-600 ml-2">({{ statsPeriod === 'daily' ? '今日' : '本月' }})</span>
</h3> </h3>
<div class="space-y-3"> <div class="space-y-3">
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<span class="text-gray-600 flex 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 输入 Token
</span> </span>
<span class="font-medium text-gray-900">{{ formatNumber(currentPeriodData.inputTokens) }}</span> <span class="font-medium text-gray-900">{{ formatNumber(currentPeriodData.inputTokens) }}</span>
</div> </div>
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<span class="text-gray-600 flex 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 输出 Token
</span> </span>
<span class="font-medium text-gray-900">{{ formatNumber(currentPeriodData.outputTokens) }}</span> <span class="font-medium text-gray-900">{{ formatNumber(currentPeriodData.outputTokens) }}</span>
</div> </div>
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<span class="text-gray-600 flex 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 缓存创建 Token
</span> </span>
<span class="font-medium text-gray-900">{{ formatNumber(currentPeriodData.cacheCreateTokens) }}</span> <span class="font-medium text-gray-900">{{ formatNumber(currentPeriodData.cacheCreateTokens) }}</span>
</div> </div>
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<span class="text-gray-600 flex 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 缓存读取 Token
</span> </span>
<span class="font-medium text-gray-900">{{ formatNumber(currentPeriodData.cacheReadTokens) }}</span> <span class="font-medium text-gray-900">{{ formatNumber(currentPeriodData.cacheReadTokens) }}</span>

View File

@@ -1,6 +1,9 @@
<template> <template>
<Teleport to="body"> <Teleport to="body">
<Transition name="modal" appear> <Transition
name="modal"
appear
>
<div <div
v-if="isVisible" v-if="isVisible"
class="fixed inset-0 modal z-[100] flex items-center justify-center p-4" 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="modal-content w-full max-w-md p-6 mx-auto">
<div class="flex items-start gap-4 mb-6"> <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"> <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>
<div class="flex-1"> <div class="flex-1">
<h3 class="text-lg font-semibold text-gray-900 mb-2">{{ title }}</h3> <h3 class="text-lg font-semibold text-gray-900 mb-2">
<div class="text-gray-600 leading-relaxed whitespace-pre-line">{{ message }}</div> {{ title }}
</h3>
<div class="text-gray-600 leading-relaxed whitespace-pre-line">
{{ message }}
</div>
</div> </div>
</div> </div>
<div class="flex items-center justify-end gap-3"> <div class="flex items-center justify-end gap-3">
<button <button
@click="handleCancel"
class="btn bg-gray-100 text-gray-700 hover:bg-gray-200 px-6 py-3" class="btn bg-gray-100 text-gray-700 hover:bg-gray-200 px-6 py-3"
:disabled="isProcessing" :disabled="isProcessing"
@click="handleCancel"
> >
{{ cancelText }} {{ cancelText }}
</button> </button>
<button <button
@click="handleConfirm"
class="btn btn-warning px-6 py-3" class="btn btn-warning px-6 py-3"
:class="{ 'opacity-50 cursor-not-allowed': isProcessing }" :class="{ 'opacity-50 cursor-not-allowed': isProcessing }"
:disabled="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 }} {{ confirmText }}
</button> </button>
</div> </div>

View File

@@ -1,27 +1,34 @@
<template> <template>
<Teleport to="body"> <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="modal-content w-full max-w-md p-6 mx-auto">
<div class="flex items-start gap-4 mb-6"> <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"> <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>
<div class="flex-1"> <div class="flex-1">
<h3 class="text-lg font-bold text-gray-900 mb-2">{{ title }}</h3> <h3 class="text-lg font-bold text-gray-900 mb-2">
<p class="text-gray-600 text-sm leading-relaxed whitespace-pre-line">{{ message }}</p> {{ title }}
</h3>
<p class="text-gray-600 text-sm leading-relaxed whitespace-pre-line">
{{ message }}
</p>
</div> </div>
</div> </div>
<div class="flex gap-3"> <div class="flex gap-3">
<button <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" 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 }} {{ cancelText }}
</button> </button>
<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" 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 }} {{ confirmText }}
</button> </button>

View File

@@ -3,27 +3,45 @@
<!-- Logo区域 --> <!-- 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"> <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"> <template v-if="!loading">
<img v-if="logoSrc" <img
:src="logoSrc" v-if="logoSrc"
alt="Logo" :src="logoSrc"
class="w-8 h-8 object-contain" alt="Logo"
@error="handleLogoError"> class="w-8 h-8 object-contain"
<i v-else class="fas fa-cloud text-xl text-gray-700"></i> @error="handleLogoError"
>
<i
v-else
class="fas fa-cloud text-xl text-gray-700"
/>
</template> </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>
<!-- 标题区域 --> <!-- 标题区域 -->
<div class="flex flex-col justify-center min-h-[48px]"> <div class="flex flex-col justify-center min-h-[48px]">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<template v-if="!loading && title"> <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> </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> </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>
</div> </div>
</template> </template>

View File

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

View File

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

View File

@@ -2,38 +2,65 @@
<div class="glass-strong rounded-3xl p-6"> <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"> <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"> <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> </h2>
<el-radio-group v-model="modelPeriod" size="small" @change="handlePeriodChange"> <el-radio-group
<el-radio-button label="daily">今日</el-radio-button> v-model="modelPeriod"
<el-radio-button label="total">累计</el-radio-button> size="small"
@change="handlePeriodChange"
>
<el-radio-button label="daily">
今日
</el-radio-button>
<el-radio-button label="total">
累计
</el-radio-button>
</el-radio-group> </el-radio-group>
</div> </div>
<div v-if="dashboardStore.dashboardModelStats.length === 0" class="text-center py-12 text-gray-500"> <div
<i class="fas fa-chart-pie text-4xl mb-3 opacity-30"></i> 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> <p>暂无模型使用数据</p>
</div> </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;"> <div
<canvas ref="chartCanvas"></canvas> class="relative"
style="height: 300px;"
>
<canvas ref="chartCanvas" />
</div> </div>
<!-- 数据列表 --> <!-- 数据列表 -->
<div class="space-y-3"> <div class="space-y-3">
<div v-for="(stat, index) in sortedStats" :key="stat.model" <div
class="flex items-center justify-between p-3 bg-gray-50 rounded-lg"> 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="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> <span class="font-medium text-gray-700">{{ stat.model }}</span>
</div> </div>
<div class="text-right"> <div class="text-right">
<p class="font-semibold text-gray-800">{{ formatNumber(stat.requests) }} 请求</p> <p class="font-semibold text-gray-800">
<p class="text-sm text-gray-500">{{ formatNumber(stat.totalTokens) }} tokens</p> {{ formatNumber(stat.requests) }} 请求
</p>
<p class="text-sm text-gray-500">
{{ formatNumber(stat.totalTokens) }} tokens
</p>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -2,24 +2,45 @@
<div class="glass-strong rounded-3xl p-6 mb-8"> <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"> <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"> <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> </h2>
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<el-radio-group v-model="granularity" size="small" @change="handleGranularityChange"> <el-radio-group
<el-radio-button label="day">按天</el-radio-button> v-model="granularity"
<el-radio-button label="hour">按小时</el-radio-button> size="small"
@change="handleGranularityChange"
>
<el-radio-button label="day">
按天
</el-radio-button>
<el-radio-button label="hour">
按小时
</el-radio-button>
</el-radio-group> </el-radio-group>
<el-select v-model="trendPeriod" size="small" style="width: 120px" @change="handlePeriodChange"> <el-select
<el-option :label="`最近${period.days}天`" :value="period.days" v-for="period in periodOptions" :key="period.days" /> 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> </el-select>
</div> </div>
</div> </div>
<div class="relative" style="height: 300px;"> <div
<canvas ref="chartCanvas"></canvas> class="relative"
style="height: 300px;"
>
<canvas ref="chartCanvas" />
</div> </div>
</div> </div>
</template> </template>

View File

@@ -1,6 +1,9 @@
<template> <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 flex-col md:flex-row justify-between items-center gap-4">
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<LogoTitle <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" 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="有新版本可用" title="有新版本可用"
> >
<i class="fas fa-arrow-up text-[10px]"></i> <i class="fas fa-arrow-up text-[10px]" />
<span>新版本</span> <span>新版本</span>
</a> </a>
</div> </div>
@@ -32,12 +35,15 @@
<!-- 用户菜单 --> <!-- 用户菜单 -->
<div class="relative user-menu-container"> <div class="relative user-menu-container">
<button <button
@click="userMenuOpen = !userMenuOpen"
class="btn btn-primary px-4 py-3 flex items-center gap-2 relative" 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> <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> </button>
<!-- 悬浮菜单 --> <!-- 悬浮菜单 -->
@@ -53,10 +59,13 @@
<span class="text-gray-500">当前版本</span> <span class="text-gray-500">当前版本</span>
<span class="font-mono text-gray-700">v{{ versionInfo.current || '...' }}</span> <span class="font-mono text-gray-700">v{{ versionInfo.current || '...' }}</span>
</div> </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"> <div class="flex items-center justify-between text-sm mb-2">
<span class="text-green-600 font-medium"> <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>
<span class="font-mono text-green-600">v{{ versionInfo.latest }}</span> <span class="font-mono text-green-600">v{{ versionInfo.latest }}</span>
</div> </div>
@@ -65,47 +74,60 @@
target="_blank" 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" 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> </a>
</div> </div>
<div v-else-if="versionInfo.checkingUpdate" class="mt-2 text-center text-xs text-gray-500"> <div
<i class="fas fa-spinner fa-spin mr-1"></i>检查更新中... 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>
<div v-else class="mt-2 text-center"> <div
v-else
class="mt-2 text-center"
>
<!-- 已是最新版提醒 --> <!-- 已是最新版提醒 -->
<transition name="fade" mode="out-in"> <transition
<div v-if="versionInfo.noUpdateMessage" key="message" class="px-3 py-1.5 bg-green-100 border border-green-200 rounded-lg inline-block"> 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"> <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> </p>
</div> </div>
<button <button
v-else v-else
key="button" key="button"
@click="checkForUpdates()"
class="text-xs text-blue-500 hover:text-blue-700 transition-colors" 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> </button>
</transition> </transition>
</div> </div>
</div> </div>
<button <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" 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> <span>修改账户信息</span>
</button> </button>
<hr class="my-2 border-gray-200"> <hr class="my-2 border-gray-200">
<button <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" 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> <span>退出登录</span>
</button> </button>
</div> </div>
@@ -114,24 +136,32 @@
</div> </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="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 justify-between mb-6">
<div class="flex items-center gap-3"> <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"> <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> </div>
<h3 class="text-xl font-bold text-gray-900">修改账户信息</h3> <h3 class="text-xl font-bold text-gray-900">
修改账户信息
</h3>
</div> </div>
<button <button
@click="closeChangePasswordModal"
class="text-gray-400 hover:text-gray-600 transition-colors" 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> </button>
</div> </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> <div>
<label class="block text-sm font-semibold text-gray-700 mb-3">当前用户名</label> <label class="block text-sm font-semibold text-gray-700 mb-3">当前用户名</label>
<input <input
@@ -140,7 +170,9 @@
disabled disabled
class="form-input w-full bg-gray-100 cursor-not-allowed" 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>
<div> <div>
@@ -151,7 +183,9 @@
class="form-input w-full" class="form-input w-full"
placeholder="输入新用户名(留空保持不变)" placeholder="输入新用户名(留空保持不变)"
> >
<p class="text-xs text-gray-500 mt-2">留空表示不修改用户名</p> <p class="text-xs text-gray-500 mt-2">
留空表示不修改用户名
</p>
</div> </div>
<div> <div>
@@ -174,7 +208,9 @@
class="form-input w-full" class="form-input w-full"
placeholder="请输入新密码" 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>
<div> <div>
@@ -191,8 +227,8 @@
<div class="flex gap-3 pt-4"> <div class="flex gap-3 pt-4">
<button <button
type="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> </button>
@@ -201,8 +237,14 @@
:disabled="changePasswordLoading" :disabled="changePasswordLoading"
class="btn btn-primary flex-1 py-3 px-6 font-semibold" class="btn btn-primary flex-1 py-3 px-6 font-semibold"
> >
<div v-if="changePasswordLoading" class="loading-spinner mr-2"></div> <div
<i v-else class="fas fa-save mr-2"></i> v-if="changePasswordLoading"
class="loading-spinner mr-2"
/>
<i
v-else
class="fas fa-save mr-2"
/>
{{ changePasswordLoading ? '保存中...' : '保存修改' }} {{ changePasswordLoading ? '保存中...' : '保存修改' }}
</button> </button>
</div> </div>

View File

@@ -4,14 +4,23 @@
<AppHeader /> <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"> <div class="tab-content">
<router-view v-slot="{ Component }"> <router-view v-slot="{ Component }">
<transition name="slide-up" mode="out-in"> <transition
name="slide-up"
mode="out-in"
>
<keep-alive :include="['DashboardView', 'ApiKeysView']"> <keep-alive :include="['DashboardView', 'ApiKeysView']">
<component :is="Component" /> <component :is="Component" />
</keep-alive> </keep-alive>

View File

@@ -3,13 +3,13 @@
<button <button
v-for="tab in tabs" v-for="tab in tabs"
:key="tab.key" :key="tab.key"
@click="$emit('tab-change', tab.key)"
:class="[ :class="[
'tab-btn flex-1 py-3 px-6 text-sm font-semibold transition-all duration-300', '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' 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> </button>
</div> </div>
</template> </template>

View File

@@ -3,199 +3,355 @@
<div class="card p-6"> <div class="card p-6">
<div class="flex flex-col md:flex-row justify-between items-center gap-4 mb-6"> <div class="flex flex-col md:flex-row justify-between items-center gap-4 mb-6">
<div> <div>
<h3 class="text-xl font-bold text-gray-900 mb-2">账户管理</h3> <h3 class="text-xl font-bold text-gray-900 mb-2">
<p class="text-gray-600">管理您的 Claude Gemini 账户及代理配置</p> 账户管理
</h3>
<p class="text-gray-600">
管理您的 Claude Gemini 账户及代理配置
</p>
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
<select v-model="accountSortBy" @change="sortAccounts()" class="form-input px-3 py-2 text-sm"> <select
<option value="name">按名称排序</option> v-model="accountSortBy"
<option value="dailyTokens">按今日Token排序</option> class="form-input px-3 py-2 text-sm"
<option value="dailyRequests">按今日请求数排序</option> @change="sortAccounts()"
<option value="totalTokens">按总Token排序</option> >
<option value="lastUsed">按最后使用排序</option> <option value="name">
按名称排序
</option>
<option value="dailyTokens">
按今日Token排序
</option>
<option value="dailyRequests">
按今日请求数排序
</option>
<option value="totalTokens">
按总Token排序
</option>
<option value="lastUsed">
按最后使用排序
</option>
</select> </select>
<button <button
@click.stop="openCreateAccountModal"
class="btn btn-success px-6 py-3 flex items-center gap-2" class="btn btn-success px-6 py-3 flex items-center gap-2"
@click.stop="openCreateAccountModal"
> >
<i class="fas fa-plus"></i>添加账户 <i class="fas fa-plus" />添加账户
</button> </button>
</div> </div>
</div> </div>
<div v-if="accountsLoading" class="text-center py-12"> <div
<div class="loading-spinner mx-auto mb-4"></div> v-if="accountsLoading"
<p class="text-gray-500">正在加载账户...</p> class="text-center py-12"
>
<div class="loading-spinner mx-auto mb-4" />
<p class="text-gray-500">
正在加载账户...
</p>
</div> </div>
<div v-else-if="sortedAccounts.length === 0" class="text-center py-12"> <div
v-else-if="sortedAccounts.length === 0"
class="text-center py-12"
>
<div class="w-16 h-16 mx-auto mb-4 bg-gray-100 rounded-full flex items-center justify-center"> <div class="w-16 h-16 mx-auto mb-4 bg-gray-100 rounded-full flex items-center justify-center">
<i class="fas fa-user-circle text-gray-400 text-xl"></i> <i class="fas fa-user-circle text-gray-400 text-xl" />
</div> </div>
<p class="text-gray-500 text-lg">暂无账户</p> <p class="text-gray-500 text-lg">
<p class="text-gray-400 text-sm mt-2">点击上方按钮添加您的第一个账户</p> 暂无账户
</p>
<p class="text-gray-400 text-sm mt-2">
点击上方按钮添加您的第一个账户
</p>
</div> </div>
<div v-else class="table-container"> <div
v-else
class="table-container"
>
<table class="min-w-full"> <table class="min-w-full">
<thead class="bg-gray-50/80 backdrop-blur-sm"> <thead class="bg-gray-50/80 backdrop-blur-sm">
<tr> <tr>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100" @click="sortAccounts('name')"> <th
class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100"
@click="sortAccounts('name')"
>
名称 名称
<i v-if="accountsSortBy === 'name'" :class="['fas', accountsSortOrder === 'asc' ? 'fa-sort-up' : 'fa-sort-down', 'ml-1']"></i> <i
<i v-else class="fas fa-sort ml-1 text-gray-400"></i> v-if="accountsSortBy === 'name'"
:class="['fas', accountsSortOrder === 'asc' ? 'fa-sort-up' : 'fa-sort-down', 'ml-1']"
/>
<i
v-else
class="fas fa-sort ml-1 text-gray-400"
/>
</th> </th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100" @click="sortAccounts('platform')"> <th
class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100"
@click="sortAccounts('platform')"
>
平台 平台
<i v-if="accountsSortBy === 'platform'" :class="['fas', accountsSortOrder === 'asc' ? 'fa-sort-up' : 'fa-sort-down', 'ml-1']"></i> <i
<i v-else class="fas fa-sort ml-1 text-gray-400"></i> v-if="accountsSortBy === 'platform'"
:class="['fas', accountsSortOrder === 'asc' ? 'fa-sort-up' : 'fa-sort-down', 'ml-1']"
/>
<i
v-else
class="fas fa-sort ml-1 text-gray-400"
/>
</th> </th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100" @click="sortAccounts('accountType')"> <th
class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100"
@click="sortAccounts('accountType')"
>
类型 类型
<i v-if="accountsSortBy === 'accountType'" :class="['fas', accountsSortOrder === 'asc' ? 'fa-sort-up' : 'fa-sort-down', 'ml-1']"></i> <i
<i v-else class="fas fa-sort ml-1 text-gray-400"></i> v-if="accountsSortBy === 'accountType'"
:class="['fas', accountsSortOrder === 'asc' ? 'fa-sort-up' : 'fa-sort-down', 'ml-1']"
/>
<i
v-else
class="fas fa-sort ml-1 text-gray-400"
/>
</th> </th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100" @click="sortAccounts('status')"> <th
class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100"
@click="sortAccounts('status')"
>
状态 状态
<i v-if="accountsSortBy === 'status'" :class="['fas', accountsSortOrder === 'asc' ? 'fa-sort-up' : 'fa-sort-down', 'ml-1']"></i> <i
<i v-else class="fas fa-sort ml-1 text-gray-400"></i> v-if="accountsSortBy === 'status'"
:class="['fas', accountsSortOrder === 'asc' ? 'fa-sort-up' : 'fa-sort-down', 'ml-1']"
/>
<i
v-else
class="fas fa-sort ml-1 text-gray-400"
/>
</th> </th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100" @click="sortAccounts('priority')"> <th
class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100"
@click="sortAccounts('priority')"
>
优先级 优先级
<i v-if="accountsSortBy === 'priority'" :class="['fas', accountsSortOrder === 'asc' ? 'fa-sort-up' : 'fa-sort-down', 'ml-1']"></i> <i
<i v-else class="fas fa-sort ml-1 text-gray-400"></i> v-if="accountsSortBy === 'priority'"
:class="['fas', accountsSortOrder === 'asc' ? 'fa-sort-up' : 'fa-sort-down', 'ml-1']"
/>
<i
v-else
class="fas fa-sort ml-1 text-gray-400"
/>
</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">
代理
</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">
今日使用
</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">
会话窗口
</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">
最后使用
</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">
操作
</th> </th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">代理</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">今日使用</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">会话窗口</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">最后使用</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">操作</th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-gray-200/50"> <tbody class="divide-y divide-gray-200/50">
<tr v-for="account in sortedAccounts" :key="account.id" class="table-row"> <tr
v-for="account in sortedAccounts"
:key="account.id"
class="table-row"
>
<td class="px-6 py-4 whitespace-nowrap"> <td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center"> <div class="flex items-center">
<div class="w-8 h-8 bg-gradient-to-br from-green-500 to-green-600 rounded-lg flex items-center justify-center mr-3"> <div class="w-8 h-8 bg-gradient-to-br from-green-500 to-green-600 rounded-lg flex items-center justify-center mr-3">
<i class="fas fa-user-circle text-white text-xs"></i> <i class="fas fa-user-circle text-white text-xs" />
</div> </div>
<div> <div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div class="text-sm font-semibold text-gray-900">{{ account.name }}</div> <div class="text-sm font-semibold text-gray-900">
<span v-if="account.accountType === 'dedicated'" {{ account.name }}
class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800"> </div>
<i class="fas fa-lock mr-1"></i>专属 <span
v-if="account.accountType === 'dedicated'"
class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800"
>
<i class="fas fa-lock mr-1" />专属
</span> </span>
<span v-else <span
class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800"> v-else
<i class="fas fa-share-alt mr-1"></i>共享 class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800"
>
<i class="fas fa-share-alt mr-1" />共享
</span> </span>
</div> </div>
<div class="text-xs text-gray-500">{{ account.id }}</div> <div class="text-xs text-gray-500">
{{ account.id }}
</div>
</div> </div>
</div> </div>
</td> </td>
<td class="px-6 py-4 whitespace-nowrap"> <td class="px-6 py-4 whitespace-nowrap">
<span v-if="account.platform === 'gemini'" <span
class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-yellow-100 text-yellow-800"> v-if="account.platform === 'gemini'"
<i class="fas fa-robot mr-1"></i>Gemini class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-yellow-100 text-yellow-800"
>
<i class="fas fa-robot mr-1" />Gemini
</span> </span>
<span v-else-if="account.platform === 'claude-console'" <span
class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-purple-100 text-purple-800"> v-else-if="account.platform === 'claude-console'"
<i class="fas fa-terminal mr-1"></i>Claude Console class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-purple-100 text-purple-800"
>
<i class="fas fa-terminal mr-1" />Claude Console
</span> </span>
<span v-else <span
class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-indigo-100 text-indigo-800"> v-else
<i class="fas fa-brain mr-1"></i>Claude class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-indigo-100 text-indigo-800"
>
<i class="fas fa-brain mr-1" />Claude
</span> </span>
</td> </td>
<td class="px-6 py-4 whitespace-nowrap"> <td class="px-6 py-4 whitespace-nowrap">
<span v-if="account.platform === 'claude-console'" <span
class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-green-100 text-green-800"> v-if="account.platform === 'claude-console'"
<i class="fas fa-key mr-1"></i>API Key class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-green-100 text-green-800"
>
<i class="fas fa-key mr-1" />API Key
</span> </span>
<span v-else-if="account.scopes && account.scopes.length > 0" <span
class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-blue-100 text-blue-800"> v-else-if="account.scopes && account.scopes.length > 0"
<i class="fas fa-lock mr-1"></i>OAuth class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-blue-100 text-blue-800"
>
<i class="fas fa-lock mr-1" />OAuth
</span> </span>
<span v-else <span
class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-orange-100 text-orange-800"> v-else
<i class="fas fa-key mr-1"></i>传统 class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-orange-100 text-orange-800"
>
<i class="fas fa-key mr-1" />传统
</span> </span>
</td> </td>
<td class="px-6 py-4 whitespace-nowrap"> <td class="px-6 py-4 whitespace-nowrap">
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<span :class="['inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold', <span
account.status === 'blocked' ? 'bg-orange-100 text-orange-800' : :class="['inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold',
account.isActive ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800']"> account.status === 'blocked' ? 'bg-orange-100 text-orange-800' :
<div :class="['w-2 h-2 rounded-full mr-2', account.isActive ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800']"
>
<div
:class="['w-2 h-2 rounded-full mr-2',
account.status === 'blocked' ? 'bg-orange-500' : account.status === 'blocked' ? 'bg-orange-500' :
account.isActive ? 'bg-green-500' : 'bg-red-500']"></div> account.isActive ? 'bg-green-500' : 'bg-red-500']"
/>
{{ account.status === 'blocked' ? '已封锁' : account.isActive ? '正常' : '异常' }} {{ account.status === 'blocked' ? '已封锁' : account.isActive ? '正常' : '异常' }}
</span> </span>
<span v-if="account.rateLimitStatus && account.rateLimitStatus.isRateLimited" <span
class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-yellow-100 text-yellow-800"> v-if="account.rateLimitStatus && account.rateLimitStatus.isRateLimited"
<i class="fas fa-exclamation-triangle mr-1"></i> class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-yellow-100 text-yellow-800"
>
<i class="fas fa-exclamation-triangle mr-1" />
限流中 ({{ account.rateLimitStatus.minutesRemaining }}分钟) 限流中 ({{ account.rateLimitStatus.minutesRemaining }}分钟)
</span> </span>
<span v-if="account.schedulable === false" <span
class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-gray-100 text-gray-700"> v-if="account.schedulable === false"
<i class="fas fa-pause-circle mr-1"></i> class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-gray-100 text-gray-700"
>
<i class="fas fa-pause-circle mr-1" />
不可调度 不可调度
</span> </span>
<span v-if="account.status === 'blocked' && account.errorMessage" <span
class="text-xs text-gray-500 mt-1 max-w-xs truncate" v-if="account.status === 'blocked' && account.errorMessage"
:title="account.errorMessage"> class="text-xs text-gray-500 mt-1 max-w-xs truncate"
:title="account.errorMessage"
>
{{ account.errorMessage }} {{ account.errorMessage }}
</span> </span>
<span v-if="account.accountType === 'dedicated'" <span
class="text-xs text-gray-500"> v-if="account.accountType === 'dedicated'"
class="text-xs text-gray-500"
>
绑定: {{ account.boundApiKeysCount || 0 }} 个API Key 绑定: {{ account.boundApiKeysCount || 0 }} 个API Key
</span> </span>
</div> </div>
</td> </td>
<td class="px-6 py-4 whitespace-nowrap"> <td class="px-6 py-4 whitespace-nowrap">
<div v-if="account.platform === 'claude' || account.platform === 'claude-console'" class="flex items-center gap-2"> <div
v-if="account.platform === 'claude' || account.platform === 'claude-console'"
class="flex items-center gap-2"
>
<div class="w-16 bg-gray-200 rounded-full h-2"> <div class="w-16 bg-gray-200 rounded-full h-2">
<div class="bg-gradient-to-r from-green-500 to-blue-600 h-2 rounded-full transition-all duration-300" <div
:style="{ width: ((101 - (account.priority || 50)) + '%') }"></div> class="bg-gradient-to-r from-green-500 to-blue-600 h-2 rounded-full transition-all duration-300"
:style="{ width: ((101 - (account.priority || 50)) + '%') }"
/>
</div> </div>
<span class="text-xs text-gray-700 font-medium min-w-[20px]"> <span class="text-xs text-gray-700 font-medium min-w-[20px]">
{{ account.priority || 50 }} {{ account.priority || 50 }}
</span> </span>
</div> </div>
<div v-else class="text-gray-400 text-sm"> <div
v-else
class="text-gray-400 text-sm"
>
<span class="text-xs">N/A</span> <span class="text-xs">N/A</span>
</div> </div>
</td> </td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-600"> <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-600">
<div v-if="formatProxyDisplay(account.proxy)" class="text-xs bg-blue-50 px-2 py-1 rounded font-mono"> <div
v-if="formatProxyDisplay(account.proxy)"
class="text-xs bg-blue-50 px-2 py-1 rounded font-mono"
>
{{ formatProxyDisplay(account.proxy) }} {{ formatProxyDisplay(account.proxy) }}
</div> </div>
<div v-else class="text-gray-400">无代理</div> <div
v-else
class="text-gray-400"
>
无代理
</div>
</td> </td>
<td class="px-6 py-4 whitespace-nowrap text-sm"> <td class="px-6 py-4 whitespace-nowrap text-sm">
<div v-if="account.usage && account.usage.daily" class="space-y-1"> <div
v-if="account.usage && account.usage.daily"
class="space-y-1"
>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div class="w-2 h-2 bg-green-500 rounded-full"></div> <div class="w-2 h-2 bg-green-500 rounded-full" />
<span class="text-sm font-medium text-gray-900">{{ account.usage.daily.requests || 0 }} </span> <span class="text-sm font-medium text-gray-900">{{ account.usage.daily.requests || 0 }} </span>
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div class="w-2 h-2 bg-blue-500 rounded-full"></div> <div class="w-2 h-2 bg-blue-500 rounded-full" />
<span class="text-xs text-gray-600">{{ formatNumber(account.usage.daily.allTokens || 0) }} tokens</span> <span class="text-xs text-gray-600">{{ formatNumber(account.usage.daily.allTokens || 0) }} tokens</span>
</div> </div>
<div v-if="account.usage.averages && account.usage.averages.rpm > 0" class="text-xs text-gray-500"> <div
v-if="account.usage.averages && account.usage.averages.rpm > 0"
class="text-xs text-gray-500"
>
平均 {{ account.usage.averages.rpm.toFixed(2) }} RPM 平均 {{ account.usage.averages.rpm.toFixed(2) }} RPM
</div> </div>
</div> </div>
<div v-else class="text-gray-400 text-xs">暂无数据</div> <div
v-else
class="text-gray-400 text-xs"
>
暂无数据
</div>
</td> </td>
<td class="px-6 py-4 whitespace-nowrap"> <td class="px-6 py-4 whitespace-nowrap">
<div v-if="account.platform === 'claude' && account.sessionWindow && account.sessionWindow.hasActiveWindow" class="space-y-2"> <div
v-if="account.platform === 'claude' && account.sessionWindow && account.sessionWindow.hasActiveWindow"
class="space-y-2"
>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div class="w-24 bg-gray-200 rounded-full h-2"> <div class="w-24 bg-gray-200 rounded-full h-2">
<div class="bg-gradient-to-r from-blue-500 to-indigo-600 h-2 rounded-full transition-all duration-300" <div
:style="{ width: account.sessionWindow.progress + '%' }"></div> class="bg-gradient-to-r from-blue-500 to-indigo-600 h-2 rounded-full transition-all duration-300"
:style="{ width: account.sessionWindow.progress + '%' }"
/>
</div> </div>
<span class="text-xs text-gray-700 font-medium min-w-[32px]"> <span class="text-xs text-gray-700 font-medium min-w-[32px]">
{{ account.sessionWindow.progress }}% {{ account.sessionWindow.progress }}%
@@ -203,15 +359,24 @@
</div> </div>
<div class="text-xs text-gray-600"> <div class="text-xs text-gray-600">
<div>{{ formatSessionWindow(account.sessionWindow.windowStart, account.sessionWindow.windowEnd) }}</div> <div>{{ formatSessionWindow(account.sessionWindow.windowStart, account.sessionWindow.windowEnd) }}</div>
<div v-if="account.sessionWindow.remainingTime > 0" class="text-indigo-600 font-medium"> <div
v-if="account.sessionWindow.remainingTime > 0"
class="text-indigo-600 font-medium"
>
剩余 {{ formatRemainingTime(account.sessionWindow.remainingTime) }} 剩余 {{ formatRemainingTime(account.sessionWindow.remainingTime) }}
</div> </div>
</div> </div>
</div> </div>
<div v-else-if="account.platform === 'claude'" class="text-gray-400 text-sm"> <div
<i class="fas fa-minus"></i> v-else-if="account.platform === 'claude'"
class="text-gray-400 text-sm"
>
<i class="fas fa-minus" />
</div> </div>
<div v-else class="text-gray-400 text-sm"> <div
v-else
class="text-gray-400 text-sm"
>
<span class="text-xs">N/A</span> <span class="text-xs">N/A</span>
</div> </div>
</td> </td>
@@ -222,7 +387,6 @@
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<button <button
v-if="account.platform === 'claude' && account.scopes" v-if="account.platform === 'claude' && account.scopes"
@click="refreshToken(account)"
:disabled="account.isRefreshing" :disabled="account.isRefreshing"
:class="[ :class="[
'px-3 py-1.5 rounded-lg text-xs font-medium transition-colors', 'px-3 py-1.5 rounded-lg text-xs font-medium transition-colors',
@@ -231,41 +395,46 @@
: 'bg-blue-100 text-blue-700 hover:bg-blue-200' : 'bg-blue-100 text-blue-700 hover:bg-blue-200'
]" ]"
:title="account.isRefreshing ? '刷新中...' : '刷新Token'" :title="account.isRefreshing ? '刷新中...' : '刷新Token'"
@click="refreshToken(account)"
> >
<i :class="[ <i
'fas fa-sync-alt', :class="[
account.isRefreshing ? 'animate-spin' : '' 'fas fa-sync-alt',
]"></i> account.isRefreshing ? 'animate-spin' : ''
]"
/>
</button> </button>
<button <button
@click="toggleSchedulable(account)"
:disabled="account.isTogglingSchedulable" :disabled="account.isTogglingSchedulable"
:class="[ :class="[
'px-3 py-1.5 rounded-lg text-xs font-medium transition-colors', 'px-3 py-1.5 rounded-lg text-xs font-medium transition-colors',
account.isTogglingSchedulable account.isTogglingSchedulable
? 'bg-gray-100 text-gray-400 cursor-not-allowed' ? 'bg-gray-100 text-gray-400 cursor-not-allowed'
: account.schedulable : account.schedulable
? 'bg-green-100 text-green-700 hover:bg-green-200' ? 'bg-green-100 text-green-700 hover:bg-green-200'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'
]" ]"
:title="account.schedulable ? '点击禁用调度' : '点击启用调度'" :title="account.schedulable ? '点击禁用调度' : '点击启用调度'"
@click="toggleSchedulable(account)"
> >
<i :class="[ <i
'fas', :class="[
account.schedulable ? 'fa-toggle-on' : 'fa-toggle-off' 'fas',
]"></i> account.schedulable ? 'fa-toggle-on' : 'fa-toggle-off'
]"
/>
</button> </button>
<button <button
@click="editAccount(account)"
class="px-3 py-1.5 bg-blue-100 text-blue-700 rounded-lg text-xs font-medium hover:bg-blue-200 transition-colors" class="px-3 py-1.5 bg-blue-100 text-blue-700 rounded-lg text-xs font-medium hover:bg-blue-200 transition-colors"
@click="editAccount(account)"
> >
<i class="fas fa-edit"></i> <i class="fas fa-edit" />
</button> </button>
<button <button
@click="deleteAccount(account)"
class="px-3 py-1.5 bg-red-100 text-red-700 rounded-lg text-xs font-medium hover:bg-red-200 transition-colors" class="px-3 py-1.5 bg-red-100 text-red-700 rounded-lg text-xs font-medium hover:bg-red-200 transition-colors"
@click="deleteAccount(account)"
> >
<i class="fas fa-trash"></i> <i class="fas fa-trash" />
</button> </button>
</div> </div>
</td> </td>

View File

@@ -3,107 +3,200 @@
<div class="card p-6"> <div class="card p-6">
<div class="flex flex-col md:flex-row justify-between items-center gap-4 mb-6"> <div class="flex flex-col md:flex-row justify-between items-center gap-4 mb-6">
<div> <div>
<h3 class="text-xl font-bold text-gray-900 mb-2">API Keys 管理</h3> <h3 class="text-xl font-bold text-gray-900 mb-2">
<p class="text-gray-600">管理和监控您的 API 密钥</p> API Keys 管理
</h3>
<p class="text-gray-600">
管理和监控您的 API 密钥
</p>
</div> </div>
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<!-- Token统计时间范围选择 --> <!-- Token统计时间范围选择 -->
<select <select
v-model="apiKeyStatsTimeRange" v-model="apiKeyStatsTimeRange"
@change="loadApiKeys()"
class="form-input px-3 py-2 text-sm" class="form-input px-3 py-2 text-sm"
@change="loadApiKeys()"
> >
<option value="today">今日</option> <option value="today">
<option value="7days">最近7天</option> 今日
<option value="monthly">本月</option> </option>
<option value="all">全部时间</option> <option value="7days">
最近7天
</option>
<option value="monthly">
本月
</option>
<option value="all">
全部时间
</option>
</select> </select>
<!-- 标签筛选器 --> <!-- 标签筛选器 -->
<select <select
v-model="selectedTagFilter" v-model="selectedTagFilter"
class="form-input px-3 py-2 text-sm" class="form-input px-3 py-2 text-sm"
> >
<option value="">所有标签</option> <option value="">
<option v-for="tag in availableTags" :key="tag" :value="tag">{{ tag }}</option> 所有标签
</option>
<option
v-for="tag in availableTags"
:key="tag"
:value="tag"
>
{{ tag }}
</option>
</select> </select>
<button <button
@click.stop="openCreateApiKeyModal"
class="btn btn-primary px-6 py-3 flex items-center gap-2" class="btn btn-primary px-6 py-3 flex items-center gap-2"
@click.stop="openCreateApiKeyModal"
> >
<i class="fas fa-plus"></i>创建新 Key <i class="fas fa-plus" />创建新 Key
</button> </button>
</div> </div>
</div> </div>
<div v-if="apiKeysLoading" class="text-center py-12"> <div
<div class="loading-spinner mx-auto mb-4"></div> v-if="apiKeysLoading"
<p class="text-gray-500">正在加载 API Keys...</p> class="text-center py-12"
>
<div class="loading-spinner mx-auto mb-4" />
<p class="text-gray-500">
正在加载 API Keys...
</p>
</div> </div>
<div v-else-if="apiKeys.length === 0" class="text-center py-12"> <div
v-else-if="apiKeys.length === 0"
class="text-center py-12"
>
<div class="w-16 h-16 mx-auto mb-4 bg-gray-100 rounded-full flex items-center justify-center"> <div class="w-16 h-16 mx-auto mb-4 bg-gray-100 rounded-full flex items-center justify-center">
<i class="fas fa-key text-gray-400 text-xl"></i> <i class="fas fa-key text-gray-400 text-xl" />
</div> </div>
<p class="text-gray-500 text-lg">暂无 API Keys</p> <p class="text-gray-500 text-lg">
<p class="text-gray-400 text-sm mt-2">点击上方按钮创建您的第一个 API Key</p> 暂无 API Keys
</p>
<p class="text-gray-400 text-sm mt-2">
点击上方按钮创建您的第一个 API Key
</p>
</div> </div>
<div v-else class="table-container"> <div
v-else
class="table-container"
>
<table class="min-w-full"> <table class="min-w-full">
<thead class="bg-gray-50/80 backdrop-blur-sm"> <thead class="bg-gray-50/80 backdrop-blur-sm">
<tr> <tr>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100" @click="sortApiKeys('name')"> <th
class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100"
@click="sortApiKeys('name')"
>
名称 名称
<i v-if="apiKeysSortBy === 'name'" :class="['fas', apiKeysSortOrder === 'asc' ? 'fa-sort-up' : 'fa-sort-down', 'ml-1']"></i> <i
<i v-else class="fas fa-sort ml-1 text-gray-400"></i> v-if="apiKeysSortBy === 'name'"
:class="['fas', apiKeysSortOrder === 'asc' ? 'fa-sort-up' : 'fa-sort-down', 'ml-1']"
/>
<i
v-else
class="fas fa-sort ml-1 text-gray-400"
/>
</th> </th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">标签</th> <th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">API Key</th> 标签
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100" @click="sortApiKeys('status')"> </th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">
API Key
</th>
<th
class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100"
@click="sortApiKeys('status')"
>
状态 状态
<i v-if="apiKeysSortBy === 'status'" :class="['fas', apiKeysSortOrder === 'asc' ? 'fa-sort-up' : 'fa-sort-down', 'ml-1']"></i> <i
<i v-else class="fas fa-sort ml-1 text-gray-400"></i> v-if="apiKeysSortBy === 'status'"
:class="['fas', apiKeysSortOrder === 'asc' ? 'fa-sort-up' : 'fa-sort-down', 'ml-1']"
/>
<i
v-else
class="fas fa-sort ml-1 text-gray-400"
/>
</th> </th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider"> <th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">
使用统计 使用统计
<span class="cursor-pointer hover:bg-gray-100 px-2 py-1 rounded" @click="sortApiKeys('cost')"> <span
class="cursor-pointer hover:bg-gray-100 px-2 py-1 rounded"
@click="sortApiKeys('cost')"
>
(费用 (费用
<i v-if="apiKeysSortBy === 'cost'" :class="['fas', apiKeysSortOrder === 'asc' ? 'fa-sort-up' : 'fa-sort-down', 'ml-1']"></i> <i
<i v-else class="fas fa-sort ml-1 text-gray-400"></i>) v-if="apiKeysSortBy === 'cost'"
:class="['fas', apiKeysSortOrder === 'asc' ? 'fa-sort-up' : 'fa-sort-down', 'ml-1']"
/>
<i
v-else
class="fas fa-sort ml-1 text-gray-400"
/>)
</span> </span>
</th> </th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100" @click="sortApiKeys('createdAt')"> <th
class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100"
@click="sortApiKeys('createdAt')"
>
创建时间 创建时间
<i v-if="apiKeysSortBy === 'createdAt'" :class="['fas', apiKeysSortOrder === 'asc' ? 'fa-sort-up' : 'fa-sort-down', 'ml-1']"></i> <i
<i v-else class="fas fa-sort ml-1 text-gray-400"></i> v-if="apiKeysSortBy === 'createdAt'"
:class="['fas', apiKeysSortOrder === 'asc' ? 'fa-sort-up' : 'fa-sort-down', 'ml-1']"
/>
<i
v-else
class="fas fa-sort ml-1 text-gray-400"
/>
</th> </th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100" @click="sortApiKeys('expiresAt')"> <th
class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100"
@click="sortApiKeys('expiresAt')"
>
过期时间 过期时间
<i v-if="apiKeysSortBy === 'expiresAt'" :class="['fas', apiKeysSortOrder === 'asc' ? 'fa-sort-up' : 'fa-sort-down', 'ml-1']"></i> <i
<i v-else class="fas fa-sort ml-1 text-gray-400"></i> v-if="apiKeysSortBy === 'expiresAt'"
:class="['fas', apiKeysSortOrder === 'asc' ? 'fa-sort-up' : 'fa-sort-down', 'ml-1']"
/>
<i
v-else
class="fas fa-sort ml-1 text-gray-400"
/>
</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">
操作
</th> </th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">操作</th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-gray-200/50"> <tbody class="divide-y divide-gray-200/50">
<template v-for="key in sortedApiKeys" :key="key.id"> <template
v-for="key in sortedApiKeys"
:key="key.id"
>
<!-- API Key 主行 --> <!-- API Key 主行 -->
<tr class="table-row"> <tr class="table-row">
<td class="px-6 py-4 whitespace-nowrap"> <td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center"> <div class="flex items-center">
<div class="w-8 h-8 bg-gradient-to-br from-blue-500 to-blue-600 rounded-lg flex items-center justify-center mr-3"> <div class="w-8 h-8 bg-gradient-to-br from-blue-500 to-blue-600 rounded-lg flex items-center justify-center mr-3">
<i class="fas fa-key text-white text-xs"></i> <i class="fas fa-key text-white text-xs" />
</div> </div>
<div> <div>
<div class="text-sm font-semibold text-gray-900">{{ key.name }}</div> <div class="text-sm font-semibold text-gray-900">
<div class="text-xs text-gray-500">{{ key.id }}</div> {{ key.name }}
</div>
<div class="text-xs text-gray-500">
{{ key.id }}
</div>
<div class="text-xs text-gray-500 mt-1"> <div class="text-xs text-gray-500 mt-1">
<span v-if="key.claudeAccountId"> <span v-if="key.claudeAccountId">
<i class="fas fa-link mr-1"></i> <i class="fas fa-link mr-1" />
绑定: {{ getBoundAccountName(key.claudeAccountId) }} 绑定: {{ getBoundAccountName(key.claudeAccountId) }}
</span> </span>
<span v-else> <span v-else>
<i class="fas fa-share-alt mr-1"></i> <i class="fas fa-share-alt mr-1" />
使用共享池 使用共享池
</span> </span>
</div> </div>
@@ -112,12 +205,17 @@
</td> </td>
<td class="px-6 py-4"> <td class="px-6 py-4">
<div class="flex flex-wrap gap-1"> <div class="flex flex-wrap gap-1">
<span v-for="tag in (key.tags || [])" :key="tag" <span
class="inline-flex items-center px-2 py-0.5 bg-blue-100 text-blue-800 text-xs rounded-full"> v-for="tag in (key.tags || [])"
:key="tag"
class="inline-flex items-center px-2 py-0.5 bg-blue-100 text-blue-800 text-xs rounded-full"
>
{{ tag }} {{ tag }}
</span> </span>
<span v-if="!key.tags || key.tags.length === 0" <span
class="text-xs text-gray-400">无标签</span> v-if="!key.tags || key.tags.length === 0"
class="text-xs text-gray-400"
>无标签</span>
</div> </div>
</td> </td>
<td class="px-6 py-4 whitespace-nowrap"> <td class="px-6 py-4 whitespace-nowrap">
@@ -126,10 +224,14 @@
</div> </div>
</td> </td>
<td class="px-6 py-4 whitespace-nowrap"> <td class="px-6 py-4 whitespace-nowrap">
<span :class="['inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold', <span
key.isActive ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800']"> :class="['inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold',
<div :class="['w-2 h-2 rounded-full mr-2', key.isActive ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800']"
key.isActive ? 'bg-green-500' : 'bg-red-500']"></div> >
<div
:class="['w-2 h-2 rounded-full mr-2',
key.isActive ? 'bg-green-500' : 'bg-red-500']"
/>
{{ key.isActive ? '活跃' : '禁用' }} {{ key.isActive ? '活跃' : '禁用' }}
</span> </span>
</td> </td>
@@ -151,7 +253,10 @@
<span class="font-medium text-green-600">{{ calculateApiKeyCost(key.usage) }}</span> <span class="font-medium text-green-600">{{ calculateApiKeyCost(key.usage) }}</span>
</div> </div>
<!-- 每日费用限制 --> <!-- 每日费用限制 -->
<div v-if="key.dailyCostLimit > 0" class="flex justify-between text-sm"> <div
v-if="key.dailyCostLimit > 0"
class="flex justify-between text-sm"
>
<span class="text-gray-600">今日费用:</span> <span class="text-gray-600">今日费用:</span>
<span :class="['font-medium', (key.dailyCost || 0) >= key.dailyCostLimit ? 'text-red-600' : 'text-blue-600']"> <span :class="['font-medium', (key.dailyCost || 0) >= key.dailyCostLimit ? 'text-red-600' : 'text-blue-600']">
${{ (key.dailyCost || 0).toFixed(2) }} / ${{ key.dailyCostLimit.toFixed(2) }} ${{ (key.dailyCost || 0).toFixed(2) }} / ${{ key.dailyCostLimit.toFixed(2) }}
@@ -167,16 +272,25 @@
<span class="text-gray-600">当前并发:</span> <span class="text-gray-600">当前并发:</span>
<span :class="['font-medium', key.currentConcurrency > 0 ? 'text-orange-600' : 'text-gray-600']"> <span :class="['font-medium', key.currentConcurrency > 0 ? 'text-orange-600' : 'text-gray-600']">
{{ key.currentConcurrency || 0 }} {{ key.currentConcurrency || 0 }}
<span v-if="key.concurrencyLimit > 0" class="text-xs text-gray-500">/ {{ key.concurrencyLimit }}</span> <span
v-if="key.concurrencyLimit > 0"
class="text-xs text-gray-500"
>/ {{ key.concurrencyLimit }}</span>
</span> </span>
</div> </div>
<!-- 时间窗口限流 --> <!-- 时间窗口限流 -->
<div v-if="key.rateLimitWindow > 0" class="flex justify-between text-sm"> <div
v-if="key.rateLimitWindow > 0"
class="flex justify-between text-sm"
>
<span class="text-gray-600">时间窗口:</span> <span class="text-gray-600">时间窗口:</span>
<span class="font-medium text-indigo-600">{{ key.rateLimitWindow }} 分钟</span> <span class="font-medium text-indigo-600">{{ key.rateLimitWindow }} 分钟</span>
</div> </div>
<!-- 请求次数限制 --> <!-- 请求次数限制 -->
<div v-if="key.rateLimitRequests > 0" class="flex justify-between text-sm"> <div
v-if="key.rateLimitRequests > 0"
class="flex justify-between text-sm"
>
<span class="text-gray-600">请求限制:</span> <span class="text-gray-600">请求限制:</span>
<span class="font-medium text-indigo-600">{{ key.rateLimitRequests }} /窗口</span> <span class="font-medium text-indigo-600">{{ key.rateLimitRequests }} /窗口</span>
</div> </div>
@@ -186,7 +300,10 @@
<span>输出: {{ formatNumber((key.usage && key.usage.total && key.usage.total.outputTokens) || 0) }}</span> <span>输出: {{ formatNumber((key.usage && key.usage.total && key.usage.total.outputTokens) || 0) }}</span>
</div> </div>
<!-- 缓存Token细节 --> <!-- 缓存Token细节 -->
<div v-if="((key.usage && key.usage.total && key.usage.total.cacheCreateTokens) || 0) > 0 || ((key.usage && key.usage.total && key.usage.total.cacheReadTokens) || 0) > 0" class="flex justify-between text-xs text-orange-500"> <div
v-if="((key.usage && key.usage.total && key.usage.total.cacheCreateTokens) || 0) > 0 || ((key.usage && key.usage.total && key.usage.total.cacheReadTokens) || 0) > 0"
class="flex justify-between text-xs text-orange-500"
>
<span>缓存创建: {{ formatNumber((key.usage && key.usage.total && key.usage.total.cacheCreateTokens) || 0) }}</span> <span>缓存创建: {{ formatNumber((key.usage && key.usage.total && key.usage.total.cacheCreateTokens) || 0) }}</span>
<span>缓存读取: {{ formatNumber((key.usage && key.usage.total && key.usage.total.cacheReadTokens) || 0) }}</span> <span>缓存读取: {{ formatNumber((key.usage && key.usage.total && key.usage.total.cacheReadTokens) || 0) }}</span>
</div> </div>
@@ -204,8 +321,12 @@
</div> </div>
<!-- 模型分布按钮 --> <!-- 模型分布按钮 -->
<div class="pt-2"> <div class="pt-2">
<button @click="toggleApiKeyModelStats(key.id)" v-if="key && key.id" class="text-xs text-indigo-600 hover:text-indigo-800 font-medium"> <button
<i :class="['fas', expandedApiKeys[key.id] ? 'fa-chevron-up' : 'fa-chevron-down', 'mr-1']"></i> v-if="key && key.id"
class="text-xs text-indigo-600 hover:text-indigo-800 font-medium"
@click="toggleApiKeyModelStats(key.id)"
>
<i :class="['fas', expandedApiKeys[key.id] ? 'fa-chevron-up' : 'fa-chevron-down', 'mr-1']" />
模型使用分布 模型使用分布
</button> </button>
</div> </div>
@@ -216,50 +337,62 @@
</td> </td>
<td class="px-6 py-4 whitespace-nowrap text-sm"> <td class="px-6 py-4 whitespace-nowrap text-sm">
<div v-if="key.expiresAt"> <div v-if="key.expiresAt">
<div v-if="isApiKeyExpired(key.expiresAt)" class="text-red-600"> <div
<i class="fas fa-exclamation-circle mr-1"></i> v-if="isApiKeyExpired(key.expiresAt)"
class="text-red-600"
>
<i class="fas fa-exclamation-circle mr-1" />
已过期 已过期
</div> </div>
<div v-else-if="isApiKeyExpiringSoon(key.expiresAt)" class="text-orange-600"> <div
<i class="fas fa-clock mr-1"></i> v-else-if="isApiKeyExpiringSoon(key.expiresAt)"
class="text-orange-600"
>
<i class="fas fa-clock mr-1" />
{{ formatExpireDate(key.expiresAt) }} {{ formatExpireDate(key.expiresAt) }}
</div> </div>
<div v-else class="text-gray-600"> <div
v-else
class="text-gray-600"
>
{{ formatExpireDate(key.expiresAt) }} {{ formatExpireDate(key.expiresAt) }}
</div> </div>
</div> </div>
<div v-else class="text-gray-400"> <div
<i class="fas fa-infinity mr-1"></i> v-else
class="text-gray-400"
>
<i class="fas fa-infinity mr-1" />
永不过期 永不过期
</div> </div>
</td> </td>
<td class="px-6 py-4 whitespace-nowrap text-sm"> <td class="px-6 py-4 whitespace-nowrap text-sm">
<div class="flex gap-2"> <div class="flex gap-2">
<button <button
@click="copyApiStatsLink(key)" class="text-purple-600 hover:text-purple-900 font-medium hover:bg-purple-50 px-3 py-1 rounded-lg transition-colors"
class="text-purple-600 hover:text-purple-900 font-medium hover:bg-purple-50 px-3 py-1 rounded-lg transition-colors"
title="复制统计页面链接" title="复制统计页面链接"
@click="copyApiStatsLink(key)"
> >
<i class="fas fa-chart-bar mr-1"></i>统计 <i class="fas fa-chart-bar mr-1" />统计
</button> </button>
<button <button
@click="openEditApiKeyModal(key)" class="text-blue-600 hover:text-blue-900 font-medium hover:bg-blue-50 px-3 py-1 rounded-lg transition-colors"
class="text-blue-600 hover:text-blue-900 font-medium hover:bg-blue-50 px-3 py-1 rounded-lg transition-colors" @click="openEditApiKeyModal(key)"
> >
<i class="fas fa-edit mr-1"></i>编辑 <i class="fas fa-edit mr-1" />编辑
</button> </button>
<button <button
v-if="key.expiresAt && (isApiKeyExpired(key.expiresAt) || isApiKeyExpiringSoon(key.expiresAt))" v-if="key.expiresAt && (isApiKeyExpired(key.expiresAt) || isApiKeyExpiringSoon(key.expiresAt))"
@click="openRenewApiKeyModal(key)" class="text-green-600 hover:text-green-900 font-medium hover:bg-green-50 px-3 py-1 rounded-lg transition-colors"
class="text-green-600 hover:text-green-900 font-medium hover:bg-green-50 px-3 py-1 rounded-lg transition-colors" @click="openRenewApiKeyModal(key)"
> >
<i class="fas fa-clock mr-1"></i>续期 <i class="fas fa-clock mr-1" />续期
</button> </button>
<button <button
@click="deleteApiKey(key.id)" class="text-red-600 hover:text-red-900 font-medium hover:bg-red-50 px-3 py-1 rounded-lg transition-colors"
class="text-red-600 hover:text-red-900 font-medium hover:bg-red-50 px-3 py-1 rounded-lg transition-colors" @click="deleteApiKey(key.id)"
> >
<i class="fas fa-trash mr-1"></i>删除 <i class="fas fa-trash mr-1" />删除
</button> </button>
</div> </div>
</td> </td>
@@ -267,20 +400,31 @@
<!-- 模型统计展开区域 --> <!-- 模型统计展开区域 -->
<tr v-if="key && key.id && expandedApiKeys[key.id]"> <tr v-if="key && key.id && expandedApiKeys[key.id]">
<td colspan="7" class="px-6 py-4 bg-gray-50"> <td
<div v-if="!apiKeyModelStats[key.id]" class="text-center py-4"> colspan="7"
<div class="loading-spinner mx-auto"></div> class="px-6 py-4 bg-gray-50"
<p class="text-sm text-gray-500 mt-2">加载模型统计...</p> >
<div
v-if="!apiKeyModelStats[key.id]"
class="text-center py-4"
>
<div class="loading-spinner mx-auto" />
<p class="text-sm text-gray-500 mt-2">
加载模型统计...
</p>
</div> </div>
<div class="space-y-4"> <div class="space-y-4">
<!-- 通用的标题和时间筛选器无论是否有数据都显示 --> <!-- 通用的标题和时间筛选器无论是否有数据都显示 -->
<div class="flex items-center justify-between mb-4"> <div class="flex items-center justify-between mb-4">
<h5 class="text-sm font-semibold text-gray-700 flex items-center"> <h5 class="text-sm font-semibold text-gray-700 flex items-center">
<i class="fas fa-chart-pie text-indigo-500 mr-2"></i> <i class="fas fa-chart-pie text-indigo-500 mr-2" />
模型使用分布 模型使用分布
</h5> </h5>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span v-if="apiKeyModelStats[key.id] && apiKeyModelStats[key.id].length > 0" class="text-xs text-gray-500 bg-gray-100 px-2 py-1 rounded-full"> <span
v-if="apiKeyModelStats[key.id] && apiKeyModelStats[key.id].length > 0"
class="text-xs text-gray-500 bg-gray-100 px-2 py-1 rounded-full"
>
{{ apiKeyModelStats[key.id].length }} 个模型 {{ apiKeyModelStats[key.id].length }} 个模型
</span> </span>
@@ -291,13 +435,13 @@
<button <button
v-for="option in getApiKeyDateFilter(key.id).presetOptions" v-for="option in getApiKeyDateFilter(key.id).presetOptions"
:key="option.value" :key="option.value"
@click="setApiKeyDateFilterPreset(option.value, key.id)"
:class="[ :class="[
'px-2 py-1 rounded text-xs font-medium transition-colors', 'px-2 py-1 rounded text-xs font-medium transition-colors',
getApiKeyDateFilter(key.id).preset === option.value && getApiKeyDateFilter(key.id).type === 'preset' getApiKeyDateFilter(key.id).preset === option.value && getApiKeyDateFilter(key.id).type === 'preset'
? 'bg-white text-blue-600 shadow-sm' ? 'bg-white text-blue-600 shadow-sm'
: 'text-gray-600 hover:text-gray-900' : 'text-gray-600 hover:text-gray-900'
]" ]"
@click="setApiKeyDateFilterPreset(option.value, key.id)"
> >
{{ option.label }} {{ option.label }}
</button> </button>
@@ -306,7 +450,6 @@
<!-- Element Plus 日期范围选择器 --> <!-- Element Plus 日期范围选择器 -->
<el-date-picker <el-date-picker
:model-value="getApiKeyDateFilter(key.id).customRange" :model-value="getApiKeyDateFilter(key.id).customRange"
@update:model-value="(value) => onApiKeyCustomDateRangeChange(key.id, value)"
type="datetimerange" type="datetimerange"
range-separator="" range-separator=""
start-placeholder="开始日期" start-placeholder="开始日期"
@@ -317,33 +460,47 @@
:default-time="defaultTime" :default-time="defaultTime"
size="small" size="small"
style="width: 280px;" style="width: 280px;"
@update:model-value="(value) => onApiKeyCustomDateRangeChange(key.id, value)"
class="api-key-date-picker" class="api-key-date-picker"
:clearable="true" :clearable="true"
:unlink-panels="false" :unlink-panels="false"
></el-date-picker> />
</div> </div>
</div> </div>
</div> </div>
<!-- 数据展示区域 --> <!-- 数据展示区域 -->
<div v-if="apiKeyModelStats[key.id] && apiKeyModelStats[key.id].length === 0" class="text-center py-8"> <div
v-if="apiKeyModelStats[key.id] && apiKeyModelStats[key.id].length === 0"
class="text-center py-8"
>
<div class="flex items-center justify-center gap-2 mb-3"> <div class="flex items-center justify-center gap-2 mb-3">
<i class="fas fa-chart-line text-gray-400 text-lg"></i> <i class="fas fa-chart-line text-gray-400 text-lg" />
<p class="text-sm text-gray-500">暂无模型使用数据</p> <p class="text-sm text-gray-500">
暂无模型使用数据
</p>
<button <button
@click="resetApiKeyDateFilter(key.id)"
class="text-blue-500 hover:text-blue-700 text-sm ml-2 flex items-center gap-1 transition-colors" class="text-blue-500 hover:text-blue-700 text-sm ml-2 flex items-center gap-1 transition-colors"
title="重置筛选条件并刷新" title="重置筛选条件并刷新"
@click="resetApiKeyDateFilter(key.id)"
> >
<i class="fas fa-sync-alt text-xs"></i> <i class="fas fa-sync-alt text-xs" />
<span class="text-xs">刷新</span> <span class="text-xs">刷新</span>
</button> </button>
</div> </div>
<p class="text-xs text-gray-400">尝试调整时间范围或点击刷新重新加载数据</p> <p class="text-xs text-gray-400">
尝试调整时间范围或点击刷新重新加载数据
</p>
</div> </div>
<div v-else-if="apiKeyModelStats[key.id] && apiKeyModelStats[key.id].length > 0" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <div
<div v-for="stat in apiKeyModelStats[key.id]" :key="stat.model" v-else-if="apiKeyModelStats[key.id] && apiKeyModelStats[key.id].length > 0"
class="bg-gradient-to-br from-white to-gray-50 rounded-xl p-4 border border-gray-200 hover:border-indigo-300 hover:shadow-lg transition-all duration-200"> class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"
>
<div
v-for="stat in apiKeyModelStats[key.id]"
:key="stat.model"
class="bg-gradient-to-br from-white to-gray-50 rounded-xl p-4 border border-gray-200 hover:border-indigo-300 hover:shadow-lg transition-all duration-200"
>
<div class="flex justify-between items-start mb-3"> <div class="flex justify-between items-start mb-3">
<div class="flex-1"> <div class="flex-1">
<span class="text-sm font-semibold text-gray-800 block mb-1">{{ stat.model }}</span> <span class="text-sm font-semibold text-gray-800 block mb-1">{{ stat.model }}</span>
@@ -354,14 +511,14 @@
<div class="space-y-2 mb-3"> <div class="space-y-2 mb-3">
<div class="flex justify-between items-center text-sm"> <div class="flex justify-between items-center text-sm">
<span class="text-gray-600 flex items-center"> <span class="text-gray-600 flex items-center">
<i class="fas fa-coins text-yellow-500 mr-1 text-xs"></i> <i class="fas fa-coins text-yellow-500 mr-1 text-xs" />
总Token: 总Token:
</span> </span>
<span class="font-semibold text-gray-900">{{ formatNumber(stat.allTokens) }}</span> <span class="font-semibold text-gray-900">{{ formatNumber(stat.allTokens) }}</span>
</div> </div>
<div class="flex justify-between items-center text-sm"> <div class="flex justify-between items-center text-sm">
<span class="text-gray-600 flex items-center"> <span class="text-gray-600 flex items-center">
<i class="fas fa-dollar-sign text-green-500 mr-1 text-xs"></i> <i class="fas fa-dollar-sign text-green-500 mr-1 text-xs" />
费用: 费用:
</span> </span>
<span class="font-semibold text-green-600">{{ calculateModelCost(stat) }}</span> <span class="font-semibold text-green-600">{{ calculateModelCost(stat) }}</span>
@@ -369,28 +526,34 @@
<div class="pt-2 mt-2 border-t border-gray-100"> <div class="pt-2 mt-2 border-t border-gray-100">
<div class="flex justify-between items-center text-xs text-gray-500"> <div class="flex justify-between items-center text-xs text-gray-500">
<span class="flex items-center"> <span class="flex items-center">
<i class="fas fa-arrow-down text-green-500 mr-1"></i> <i class="fas fa-arrow-down text-green-500 mr-1" />
输入: 输入:
</span> </span>
<span class="font-medium">{{ formatNumber(stat.inputTokens) }}</span> <span class="font-medium">{{ formatNumber(stat.inputTokens) }}</span>
</div> </div>
<div class="flex justify-between items-center text-xs text-gray-500"> <div class="flex justify-between items-center text-xs text-gray-500">
<span class="flex items-center"> <span class="flex items-center">
<i class="fas fa-arrow-up text-blue-500 mr-1"></i> <i class="fas fa-arrow-up text-blue-500 mr-1" />
输出: 输出:
</span> </span>
<span class="font-medium">{{ formatNumber(stat.outputTokens) }}</span> <span class="font-medium">{{ formatNumber(stat.outputTokens) }}</span>
</div> </div>
<div v-if="stat.cacheCreateTokens > 0" class="flex justify-between items-center text-xs text-purple-600"> <div
v-if="stat.cacheCreateTokens > 0"
class="flex justify-between items-center text-xs text-purple-600"
>
<span class="flex items-center"> <span class="flex items-center">
<i class="fas fa-save mr-1"></i> <i class="fas fa-save mr-1" />
缓存创建: 缓存创建:
</span> </span>
<span class="font-medium">{{ formatNumber(stat.cacheCreateTokens) }}</span> <span class="font-medium">{{ formatNumber(stat.cacheCreateTokens) }}</span>
</div> </div>
<div v-if="stat.cacheReadTokens > 0" class="flex justify-between items-center text-xs text-purple-600"> <div
v-if="stat.cacheReadTokens > 0"
class="flex justify-between items-center text-xs text-purple-600"
>
<span class="flex items-center"> <span class="flex items-center">
<i class="fas fa-download mr-1"></i> <i class="fas fa-download mr-1" />
缓存读取: 缓存读取:
</span> </span>
<span class="font-medium">{{ formatNumber(stat.cacheReadTokens) }}</span> <span class="font-medium">{{ formatNumber(stat.cacheReadTokens) }}</span>
@@ -400,9 +563,10 @@
<!-- 进度条 --> <!-- 进度条 -->
<div class="w-full bg-gray-200 rounded-full h-2 mt-3"> <div class="w-full bg-gray-200 rounded-full h-2 mt-3">
<div class="bg-gradient-to-r from-indigo-500 to-purple-600 h-2 rounded-full transition-all duration-500" <div
:style="{ width: calculateApiKeyModelPercentage(stat.allTokens, apiKeyModelStats[key.id]) + '%' }"> class="bg-gradient-to-r from-indigo-500 to-purple-600 h-2 rounded-full transition-all duration-500"
</div> :style="{ width: calculateApiKeyModelPercentage(stat.allTokens, apiKeyModelStats[key.id]) + '%' }"
/>
</div> </div>
<div class="text-right mt-1"> <div class="text-right mt-1">
<span class="text-xs font-medium text-indigo-600"> <span class="text-xs font-medium text-indigo-600">
@@ -413,10 +577,13 @@
</div> </div>
<!-- 总计统计,仅在有数据时显示 --> <!-- 总计统计,仅在有数据时显示 -->
<div v-if="apiKeyModelStats[key.id] && apiKeyModelStats[key.id].length > 0" class="mt-4 p-3 bg-gradient-to-r from-indigo-50 to-purple-50 rounded-lg border border-indigo-100"> <div
v-if="apiKeyModelStats[key.id] && apiKeyModelStats[key.id].length > 0"
class="mt-4 p-3 bg-gradient-to-r from-indigo-50 to-purple-50 rounded-lg border border-indigo-100"
>
<div class="flex items-center justify-between text-sm"> <div class="flex items-center justify-between text-sm">
<span class="font-semibold text-gray-700 flex items-center"> <span class="font-semibold text-gray-700 flex items-center">
<i class="fas fa-calculator text-indigo-500 mr-2"></i> <i class="fas fa-calculator text-indigo-500 mr-2" />
总计统计 总计统计
</span> </span>
<div class="flex gap-4 text-xs"> <div class="flex gap-4 text-xs">
@@ -448,7 +615,7 @@
<EditApiKeyModal <EditApiKeyModal
v-if="showEditApiKeyModal" v-if="showEditApiKeyModal"
:apiKey="editingApiKey" :api-key="editingApiKey"
:accounts="accounts" :accounts="accounts"
@close="showEditApiKeyModal = false" @close="showEditApiKeyModal = false"
@success="handleEditSuccess" @success="handleEditSuccess"
@@ -456,14 +623,14 @@
<RenewApiKeyModal <RenewApiKeyModal
v-if="showRenewApiKeyModal" v-if="showRenewApiKeyModal"
:apiKey="renewingApiKey" :api-key="renewingApiKey"
@close="showRenewApiKeyModal = false" @close="showRenewApiKeyModal = false"
@success="handleRenewSuccess" @success="handleRenewSuccess"
/> />
<NewApiKeyModal <NewApiKeyModal
v-if="showNewApiKeyModal" v-if="showNewApiKeyModal"
:apiKey="newApiKeyData" :api-key="newApiKeyData"
@close="showNewApiKeyModal = false" @close="showNewApiKeyModal = false"
/> />
</div> </div>

View File

@@ -10,8 +10,11 @@
:logo-src="oemSettings.siteIconData || oemSettings.siteIcon" :logo-src="oemSettings.siteIconData || oemSettings.siteIcon"
/> />
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<router-link to="/dashboard" class="admin-button rounded-xl px-4 py-2 text-white transition-all duration-300 flex items-center gap-2"> <router-link
<i class="fas fa-cog text-sm"></i> to="/dashboard"
class="admin-button rounded-xl px-4 py-2 text-white transition-all duration-300 flex items-center gap-2"
>
<i class="fas fa-cog text-sm" />
<span class="text-sm font-medium">管理后台</span> <span class="text-sm font-medium">管理后台</span>
</router-link> </router-link>
</div> </div>
@@ -23,23 +26,23 @@
<div class="flex justify-center"> <div class="flex justify-center">
<div class="inline-flex bg-white/10 backdrop-blur-xl rounded-full p-1 shadow-lg border border-white/20"> <div class="inline-flex bg-white/10 backdrop-blur-xl rounded-full p-1 shadow-lg border border-white/20">
<button <button
@click="currentTab = 'stats'"
:class="[ :class="[
'tab-pill-button', 'tab-pill-button',
currentTab === 'stats' ? 'active' : '' currentTab === 'stats' ? 'active' : ''
]" ]"
@click="currentTab = 'stats'"
> >
<i class="fas fa-chart-line mr-2"></i> <i class="fas fa-chart-line mr-2" />
<span>统计查询</span> <span>统计查询</span>
</button> </button>
<button <button
@click="currentTab = 'tutorial'"
:class="[ :class="[
'tab-pill-button', 'tab-pill-button',
currentTab === 'tutorial' ? 'active' : '' currentTab === 'tutorial' ? 'active' : ''
]" ]"
@click="currentTab = 'tutorial'"
> >
<i class="fas fa-graduation-cap mr-2"></i> <i class="fas fa-graduation-cap mr-2" />
<span>使用教程</span> <span>使用教程</span>
</button> </button>
</div> </div>
@@ -47,45 +50,54 @@
</div> </div>
<!-- 统计内容 --> <!-- 统计内容 -->
<div v-if="currentTab === 'stats'" class="tab-content"> <div
v-if="currentTab === 'stats'"
class="tab-content"
>
<!-- API Key 输入区域 --> <!-- API Key 输入区域 -->
<ApiKeyInput /> <ApiKeyInput />
<!-- 错误提示 --> <!-- 错误提示 -->
<div v-if="error" class="mb-8"> <div
v-if="error"
class="mb-8"
>
<div class="bg-red-500/20 border border-red-500/30 rounded-xl p-4 text-red-800 backdrop-blur-sm"> <div class="bg-red-500/20 border border-red-500/30 rounded-xl p-4 text-red-800 backdrop-blur-sm">
<i class="fas fa-exclamation-triangle mr-2"></i> <i class="fas fa-exclamation-triangle mr-2" />
{{ error }} {{ error }}
</div> </div>
</div> </div>
<!-- 统计数据展示区域 --> <!-- 统计数据展示区域 -->
<div v-if="statsData" class="fade-in"> <div
v-if="statsData"
class="fade-in"
>
<div class="glass-strong rounded-3xl p-6 shadow-xl"> <div class="glass-strong rounded-3xl p-6 shadow-xl">
<!-- 时间范围选择器 --> <!-- 时间范围选择器 -->
<div class="mb-6 pb-6 border-b border-gray-200"> <div class="mb-6 pb-6 border-b border-gray-200">
<div class="flex flex-col md:flex-row items-start md:items-center justify-between gap-4"> <div class="flex flex-col md:flex-row items-start md:items-center justify-between gap-4">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<i class="fas fa-clock text-blue-500 text-lg"></i> <i class="fas fa-clock text-blue-500 text-lg" />
<span class="text-lg font-medium text-gray-700">统计时间范围</span> <span class="text-lg font-medium text-gray-700">统计时间范围</span>
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
<button <button
@click="switchPeriod('daily')"
:class="['period-btn', { 'active': statsPeriod === 'daily' }]" :class="['period-btn', { 'active': statsPeriod === 'daily' }]"
class="px-6 py-2 text-sm font-medium flex items-center gap-2" class="px-6 py-2 text-sm font-medium flex items-center gap-2"
:disabled="loading || modelStatsLoading" :disabled="loading || modelStatsLoading"
@click="switchPeriod('daily')"
> >
<i class="fas fa-calendar-day"></i> <i class="fas fa-calendar-day" />
今日 今日
</button> </button>
<button <button
@click="switchPeriod('monthly')"
:class="['period-btn', { 'active': statsPeriod === 'monthly' }]" :class="['period-btn', { 'active': statsPeriod === 'monthly' }]"
class="px-6 py-2 text-sm font-medium flex items-center gap-2" class="px-6 py-2 text-sm font-medium flex items-center gap-2"
:disabled="loading || modelStatsLoading" :disabled="loading || modelStatsLoading"
@click="switchPeriod('monthly')"
> >
<i class="fas fa-calendar-alt"></i> <i class="fas fa-calendar-alt" />
本月 本月
</button> </button>
</div> </div>
@@ -108,7 +120,10 @@
</div> </div>
<!-- 教程内容 --> <!-- 教程内容 -->
<div v-if="currentTab === 'tutorial'" class="tab-content"> <div
v-if="currentTab === 'tutorial'"
class="tab-content"
>
<div class="glass-strong rounded-3xl shadow-xl"> <div class="glass-strong rounded-3xl shadow-xl">
<TutorialView /> <TutorialView />
</div> </div>

View File

@@ -1,56 +1,22 @@
<template> <template>
<div> <div>
<!-- 自动刷新控制栏 -->
<div class="mb-6 bg-white rounded-lg shadow-sm p-4">
<div class="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
<div class="flex items-center gap-2 sm:gap-4">
<h2 class="text-lg font-semibold text-gray-800">系统仪表盘</h2>
<div v-if="refreshCountdownDisplay" class="text-sm text-gray-500 whitespace-nowrap">
<i class="fas fa-clock"></i>
{{ refreshCountdownDisplay }}
</div>
</div>
<div class="flex items-center gap-2 sm:gap-4 w-full sm:w-auto">
<!-- 手动刷新按钮 -->
<button
@click="refreshAllData"
:disabled="isRefreshing"
class="px-4 py-2 text-sm bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
>
<i :class="['fas fa-sync-alt', { 'animate-spin': isRefreshing }]"></i>
{{ isRefreshing ? '刷新中...' : '刷新数据' }}
</button>
<!-- 自动刷新开关 -->
<div class="flex items-center gap-2">
<label class="flex items-center cursor-pointer">
<input
type="checkbox"
v-model="autoRefreshEnabled"
class="sr-only peer"
>
<div class="relative w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
<span class="ml-3 text-sm font-medium text-gray-700">
自动刷新
<span class="text-xs text-gray-500">(30)</span>
</span>
</label>
</div>
</div>
</div>
</div>
<!-- 主要统计 --> <!-- 主要统计 -->
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6 mb-8"> <div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6 mb-8">
<div class="stat-card"> <div class="stat-card">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <div>
<p class="text-sm font-semibold text-gray-600 mb-1">总API Keys</p> <p class="text-sm font-semibold text-gray-600 mb-1">
<p class="text-3xl font-bold text-gray-900">{{ dashboardData.totalApiKeys }}</p> 总API Keys
<p class="text-xs text-gray-500 mt-1">活跃: {{ dashboardData.activeApiKeys || 0 }}</p> </p>
<p class="text-3xl font-bold text-gray-900">
{{ dashboardData.totalApiKeys }}
</p>
<p class="text-xs text-gray-500 mt-1">
活跃: {{ dashboardData.activeApiKeys || 0 }}
</p>
</div> </div>
<div class="stat-icon flex-shrink-0 bg-gradient-to-br from-blue-500 to-blue-600"> <div class="stat-icon flex-shrink-0 bg-gradient-to-br from-blue-500 to-blue-600">
<i class="fas fa-key"></i> <i class="fas fa-key" />
</div> </div>
</div> </div>
</div> </div>
@@ -58,17 +24,24 @@
<div class="stat-card"> <div class="stat-card">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <div>
<p class="text-sm font-semibold text-gray-600 mb-1">服务账户</p> <p class="text-sm font-semibold text-gray-600 mb-1">
<p class="text-3xl font-bold text-gray-900">{{ dashboardData.totalAccounts }}</p> 服务账户
</p>
<p class="text-3xl font-bold text-gray-900">
{{ dashboardData.totalAccounts }}
</p>
<p class="text-xs text-gray-500 mt-1"> <p class="text-xs text-gray-500 mt-1">
活跃: {{ dashboardData.activeAccounts || 0 }} 活跃: {{ dashboardData.activeAccounts || 0 }}
<span v-if="dashboardData.rateLimitedAccounts > 0" class="text-yellow-600"> <span
v-if="dashboardData.rateLimitedAccounts > 0"
class="text-yellow-600"
>
| 限流: {{ dashboardData.rateLimitedAccounts }} | 限流: {{ dashboardData.rateLimitedAccounts }}
</span> </span>
</p> </p>
</div> </div>
<div class="stat-icon flex-shrink-0 bg-gradient-to-br from-green-500 to-green-600"> <div class="stat-icon flex-shrink-0 bg-gradient-to-br from-green-500 to-green-600">
<i class="fas fa-user-circle"></i> <i class="fas fa-user-circle" />
</div> </div>
</div> </div>
</div> </div>
@@ -76,12 +49,18 @@
<div class="stat-card"> <div class="stat-card">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <div>
<p class="text-sm font-semibold text-gray-600 mb-1">今日请求</p> <p class="text-sm font-semibold text-gray-600 mb-1">
<p class="text-3xl font-bold text-gray-900">{{ dashboardData.todayRequests }}</p> 今日请求
<p class="text-xs text-gray-500 mt-1">总请求: {{ formatNumber(dashboardData.totalRequests || 0) }}</p> </p>
<p class="text-3xl font-bold text-gray-900">
{{ dashboardData.todayRequests }}
</p>
<p class="text-xs text-gray-500 mt-1">
总请求: {{ formatNumber(dashboardData.totalRequests || 0) }}
</p>
</div> </div>
<div class="stat-icon flex-shrink-0 bg-gradient-to-br from-purple-500 to-purple-600"> <div class="stat-icon flex-shrink-0 bg-gradient-to-br from-purple-500 to-purple-600">
<i class="fas fa-chart-line"></i> <i class="fas fa-chart-line" />
</div> </div>
</div> </div>
</div> </div>
@@ -89,12 +68,18 @@
<div class="stat-card"> <div class="stat-card">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <div>
<p class="text-sm font-semibold text-gray-600 mb-1">系统状态</p> <p class="text-sm font-semibold text-gray-600 mb-1">
<p class="text-3xl font-bold text-green-600">{{ dashboardData.systemStatus }}</p> 系统状态
<p class="text-xs text-gray-500 mt-1">运行时间: {{ formattedUptime }}</p> </p>
<p class="text-3xl font-bold text-green-600">
{{ dashboardData.systemStatus }}
</p>
<p class="text-xs text-gray-500 mt-1">
运行时间: {{ formattedUptime }}
</p>
</div> </div>
<div class="stat-icon flex-shrink-0 bg-gradient-to-br from-yellow-500 to-orange-500"> <div class="stat-icon flex-shrink-0 bg-gradient-to-br from-yellow-500 to-orange-500">
<i class="fas fa-heartbeat"></i> <i class="fas fa-heartbeat" />
</div> </div>
</div> </div>
</div> </div>
@@ -105,22 +90,32 @@
<div class="stat-card"> <div class="stat-card">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="flex-1 mr-8"> <div class="flex-1 mr-8">
<p class="text-sm font-semibold text-gray-600 mb-1">今日Token</p> <p class="text-sm font-semibold text-gray-600 mb-1">
今日Token
</p>
<div class="flex items-baseline gap-2 mb-2 flex-wrap"> <div class="flex items-baseline gap-2 mb-2 flex-wrap">
<p class="text-3xl font-bold text-blue-600">{{ formatNumber((dashboardData.todayInputTokens || 0) + (dashboardData.todayOutputTokens || 0) + (dashboardData.todayCacheCreateTokens || 0) + (dashboardData.todayCacheReadTokens || 0)) }}</p> <p class="text-3xl font-bold text-blue-600">
{{ formatNumber((dashboardData.todayInputTokens || 0) + (dashboardData.todayOutputTokens || 0) + (dashboardData.todayCacheCreateTokens || 0) + (dashboardData.todayCacheReadTokens || 0)) }}
</p>
<span class="text-sm text-green-600 font-medium">/ {{ costsData.todayCosts.formatted.totalCost }}</span> <span class="text-sm text-green-600 font-medium">/ {{ costsData.todayCosts.formatted.totalCost }}</span>
</div> </div>
<div class="text-xs text-gray-500"> <div class="text-xs text-gray-500">
<div class="flex justify-between items-center flex-wrap gap-x-4"> <div class="flex justify-between items-center flex-wrap gap-x-4">
<span>输入: <span class="font-medium">{{ formatNumber(dashboardData.todayInputTokens || 0) }}</span></span> <span>输入: <span class="font-medium">{{ formatNumber(dashboardData.todayInputTokens || 0) }}</span></span>
<span>输出: <span class="font-medium">{{ formatNumber(dashboardData.todayOutputTokens || 0) }}</span></span> <span>输出: <span class="font-medium">{{ formatNumber(dashboardData.todayOutputTokens || 0) }}</span></span>
<span v-if="(dashboardData.todayCacheCreateTokens || 0) > 0" class="text-purple-600">缓存创建: <span class="font-medium">{{ formatNumber(dashboardData.todayCacheCreateTokens || 0) }}</span></span> <span
<span v-if="(dashboardData.todayCacheReadTokens || 0) > 0" class="text-purple-600">缓存读取: <span class="font-medium">{{ formatNumber(dashboardData.todayCacheReadTokens || 0) }}</span></span> v-if="(dashboardData.todayCacheCreateTokens || 0) > 0"
class="text-purple-600"
>缓存创建: <span class="font-medium">{{ formatNumber(dashboardData.todayCacheCreateTokens || 0) }}</span></span>
<span
v-if="(dashboardData.todayCacheReadTokens || 0) > 0"
class="text-purple-600"
>缓存读取: <span class="font-medium">{{ formatNumber(dashboardData.todayCacheReadTokens || 0) }}</span></span>
</div> </div>
</div> </div>
</div> </div>
<div class="stat-icon flex-shrink-0 bg-gradient-to-br from-indigo-500 to-indigo-600"> <div class="stat-icon flex-shrink-0 bg-gradient-to-br from-indigo-500 to-indigo-600">
<i class="fas fa-coins"></i> <i class="fas fa-coins" />
</div> </div>
</div> </div>
</div> </div>
@@ -128,22 +123,32 @@
<div class="stat-card"> <div class="stat-card">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="flex-1 mr-8"> <div class="flex-1 mr-8">
<p class="text-sm font-semibold text-gray-600 mb-1">总Token消耗</p> <p class="text-sm font-semibold text-gray-600 mb-1">
总Token消耗
</p>
<div class="flex items-baseline gap-2 mb-2 flex-wrap"> <div class="flex items-baseline gap-2 mb-2 flex-wrap">
<p class="text-3xl font-bold text-emerald-600">{{ formatNumber((dashboardData.totalInputTokens || 0) + (dashboardData.totalOutputTokens || 0) + (dashboardData.totalCacheCreateTokens || 0) + (dashboardData.totalCacheReadTokens || 0)) }}</p> <p class="text-3xl font-bold text-emerald-600">
{{ formatNumber((dashboardData.totalInputTokens || 0) + (dashboardData.totalOutputTokens || 0) + (dashboardData.totalCacheCreateTokens || 0) + (dashboardData.totalCacheReadTokens || 0)) }}
</p>
<span class="text-sm text-green-600 font-medium">/ {{ costsData.totalCosts.formatted.totalCost }}</span> <span class="text-sm text-green-600 font-medium">/ {{ costsData.totalCosts.formatted.totalCost }}</span>
</div> </div>
<div class="text-xs text-gray-500"> <div class="text-xs text-gray-500">
<div class="flex justify-between items-center flex-wrap gap-x-4"> <div class="flex justify-between items-center flex-wrap gap-x-4">
<span>输入: <span class="font-medium">{{ formatNumber(dashboardData.totalInputTokens || 0) }}</span></span> <span>输入: <span class="font-medium">{{ formatNumber(dashboardData.totalInputTokens || 0) }}</span></span>
<span>输出: <span class="font-medium">{{ formatNumber(dashboardData.totalOutputTokens || 0) }}</span></span> <span>输出: <span class="font-medium">{{ formatNumber(dashboardData.totalOutputTokens || 0) }}</span></span>
<span v-if="(dashboardData.totalCacheCreateTokens || 0) > 0" class="text-purple-600">缓存创建: <span class="font-medium">{{ formatNumber(dashboardData.totalCacheCreateTokens || 0) }}</span></span> <span
<span v-if="(dashboardData.totalCacheReadTokens || 0) > 0" class="text-purple-600">缓存读取: <span class="font-medium">{{ formatNumber(dashboardData.totalCacheReadTokens || 0) }}</span></span> v-if="(dashboardData.totalCacheCreateTokens || 0) > 0"
class="text-purple-600"
>缓存创建: <span class="font-medium">{{ formatNumber(dashboardData.totalCacheCreateTokens || 0) }}</span></span>
<span
v-if="(dashboardData.totalCacheReadTokens || 0) > 0"
class="text-purple-600"
>缓存读取: <span class="font-medium">{{ formatNumber(dashboardData.totalCacheReadTokens || 0) }}</span></span>
</div> </div>
</div> </div>
</div> </div>
<div class="stat-icon flex-shrink-0 bg-gradient-to-br from-emerald-500 to-emerald-600"> <div class="stat-icon flex-shrink-0 bg-gradient-to-br from-emerald-500 to-emerald-600">
<i class="fas fa-database"></i> <i class="fas fa-database" />
</div> </div>
</div> </div>
</div> </div>
@@ -155,16 +160,21 @@
实时RPM 实时RPM
<span class="text-xs text-gray-400">({{ dashboardData.metricsWindow }}分钟)</span> <span class="text-xs text-gray-400">({{ dashboardData.metricsWindow }}分钟)</span>
</p> </p>
<p class="text-3xl font-bold text-orange-600">{{ dashboardData.realtimeRPM || 0 }}</p> <p class="text-3xl font-bold text-orange-600">
{{ dashboardData.realtimeRPM || 0 }}
</p>
<p class="text-xs text-gray-500 mt-1"> <p class="text-xs text-gray-500 mt-1">
每分钟请求数 每分钟请求数
<span v-if="dashboardData.isHistoricalMetrics" class="text-yellow-600"> <span
<i class="fas fa-exclamation-circle"></i> 历史数据 v-if="dashboardData.isHistoricalMetrics"
class="text-yellow-600"
>
<i class="fas fa-exclamation-circle" /> 历史数据
</span> </span>
</p> </p>
</div> </div>
<div class="stat-icon flex-shrink-0 bg-gradient-to-br from-orange-500 to-orange-600"> <div class="stat-icon flex-shrink-0 bg-gradient-to-br from-orange-500 to-orange-600">
<i class="fas fa-tachometer-alt"></i> <i class="fas fa-tachometer-alt" />
</div> </div>
</div> </div>
</div> </div>
@@ -176,16 +186,21 @@
实时TPM 实时TPM
<span class="text-xs text-gray-400">({{ dashboardData.metricsWindow }}分钟)</span> <span class="text-xs text-gray-400">({{ dashboardData.metricsWindow }}分钟)</span>
</p> </p>
<p class="text-3xl font-bold text-rose-600">{{ formatNumber(dashboardData.realtimeTPM || 0) }}</p> <p class="text-3xl font-bold text-rose-600">
{{ formatNumber(dashboardData.realtimeTPM || 0) }}
</p>
<p class="text-xs text-gray-500 mt-1"> <p class="text-xs text-gray-500 mt-1">
每分钟Token数 每分钟Token数
<span v-if="dashboardData.isHistoricalMetrics" class="text-yellow-600"> <span
<i class="fas fa-exclamation-circle"></i> 历史数据 v-if="dashboardData.isHistoricalMetrics"
class="text-yellow-600"
>
<i class="fas fa-exclamation-circle" /> 历史数据
</span> </span>
</p> </p>
</div> </div>
<div class="stat-icon flex-shrink-0 bg-gradient-to-br from-rose-500 to-rose-600"> <div class="stat-icon flex-shrink-0 bg-gradient-to-br from-rose-500 to-rose-600">
<i class="fas fa-rocket"></i> <i class="fas fa-rocket" />
</div> </div>
</div> </div>
</div> </div>
@@ -193,21 +208,23 @@
<!-- 模型消费统计 --> <!-- 模型消费统计 -->
<div class="mb-8"> <div class="mb-8">
<div class="flex items-center justify-between mb-6"> <div class="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4 mb-6">
<h3 class="text-xl font-bold text-gray-900">模型使用分布与Token使用趋势</h3> <h3 class="text-xl font-bold text-gray-900">
<div class="flex gap-2 items-center"> 模型使用分布与Token使用趋势
</h3>
<div class="flex flex-wrap gap-2 items-center">
<!-- 快捷日期选择 --> <!-- 快捷日期选择 -->
<div class="flex gap-1 bg-gray-100 rounded-lg p-1"> <div class="flex gap-1 bg-gray-100 rounded-lg p-1">
<button <button
v-for="option in dateFilter.presetOptions" v-for="option in dateFilter.presetOptions"
:key="option.value" :key="option.value"
@click="setDateFilterPreset(option.value)"
:class="[ :class="[
'px-3 py-1 rounded-md text-sm font-medium transition-colors', 'px-3 py-1 rounded-md text-sm font-medium transition-colors',
dateFilter.preset === option.value && dateFilter.type === 'preset' dateFilter.preset === option.value && dateFilter.type === 'preset'
? 'bg-white text-blue-600 shadow-sm' ? 'bg-white text-blue-600 shadow-sm'
: 'text-gray-600 hover:text-gray-900' : 'text-gray-600 hover:text-gray-900'
]" ]"
@click="setDateFilterPreset(option.value)"
> >
{{ option.label }} {{ option.label }}
</button> </button>
@@ -216,94 +233,163 @@
<!-- 粒度切换按钮 --> <!-- 粒度切换按钮 -->
<div class="flex gap-1 bg-gray-100 rounded-lg p-1"> <div class="flex gap-1 bg-gray-100 rounded-lg p-1">
<button <button
@click="setTrendGranularity('day')"
:class="[ :class="[
'px-3 py-1 rounded-md text-sm font-medium transition-colors', 'px-3 py-1 rounded-md text-sm font-medium transition-colors',
trendGranularity === 'day' trendGranularity === 'day'
? 'bg-white text-blue-600 shadow-sm' ? 'bg-white text-blue-600 shadow-sm'
: 'text-gray-600 hover:text-gray-900' : 'text-gray-600 hover:text-gray-900'
]" ]"
@click="setTrendGranularity('day')"
> >
<i class="fas fa-calendar-day mr-1"></i>按天 <i class="fas fa-calendar-day mr-1" />按天
</button> </button>
<button <button
@click="setTrendGranularity('hour')"
:class="[ :class="[
'px-3 py-1 rounded-md text-sm font-medium transition-colors', 'px-3 py-1 rounded-md text-sm font-medium transition-colors',
trendGranularity === 'hour' trendGranularity === 'hour'
? 'bg-white text-blue-600 shadow-sm' ? 'bg-white text-blue-600 shadow-sm'
: 'text-gray-600 hover:text-gray-900' : 'text-gray-600 hover:text-gray-900'
]" ]"
@click="setTrendGranularity('hour')"
> >
<i class="fas fa-clock mr-1"></i>按小时 <i class="fas fa-clock mr-1" />按小时
</button> </button>
</div> </div>
<!-- Element Plus 日期范围选择器 --> <!-- Element Plus 日期范围选择器 -->
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<el-date-picker <el-date-picker
:default-time="defaultTime"
v-model="dateFilter.customRange" v-model="dateFilter.customRange"
:default-time="defaultTime"
type="datetimerange" type="datetimerange"
range-separator="" range-separator=""
start-placeholder="开始日期" start-placeholder="开始日期"
end-placeholder="结束日期" end-placeholder="结束日期"
format="YYYY-MM-DD HH:mm:ss" format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss" value-format="YYYY-MM-DD HH:mm:ss"
@change="onCustomDateRangeChange"
:disabled-date="disabledDate" :disabled-date="disabledDate"
size="default" size="default"
style="width: 400px;" style="width: 400px;"
class="custom-date-picker" class="custom-date-picker"
></el-date-picker> @change="onCustomDateRangeChange"
<span v-if="trendGranularity === 'hour'" class="text-xs text-orange-600"> />
<i class="fas fa-info-circle"></i> 最多24小时 <span
v-if="trendGranularity === 'hour'"
class="text-xs text-orange-600"
>
<i class="fas fa-info-circle" /> 最多24小时
</span> </span>
</div> </div>
<button <!-- 刷新控制 -->
@click="refreshAllData()" <div class="flex items-center gap-2">
:disabled="isRefreshing" <!-- 自动刷新控制 -->
class="btn btn-primary px-4 py-2 flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed" <div class="flex items-center bg-gray-100 rounded-lg px-3 py-1">
> <label class="relative inline-flex items-center cursor-pointer">
<i :class="['fas fa-sync-alt', { 'animate-spin': isRefreshing }]"></i> <input
{{ isRefreshing ? '刷新中' : '刷新' }} v-model="autoRefreshEnabled"
</button> type="checkbox"
class="sr-only peer"
>
<!-- 更小的开关 -->
<div class="relative w-9 h-5 bg-gray-300 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-blue-300 rounded-full peer peer-checked:bg-blue-500 transition-all duration-200 after:content-[''] after:absolute after:top-0.5 after:left-[2px] after:bg-white after:w-4 after:h-4 after:rounded-full after:shadow-sm after:transition-transform after:duration-200 peer-checked:after:translate-x-4" />
<span class="ml-2.5 text-sm font-medium text-gray-600 select-none flex items-center gap-1">
<i class="fas fa-redo-alt text-xs text-gray-500" />
<span>自动刷新</span>
<span
v-if="autoRefreshEnabled"
class="ml-1 text-xs text-blue-600 font-mono transition-opacity"
:class="refreshCountdown > 0 ? 'opacity-100' : 'opacity-0'"
>
{{ refreshCountdown }}s
</span>
</span>
</label>
</div>
<!-- 刷新按钮 -->
<button
:disabled="isRefreshing"
class="px-3 py-1 rounded-md text-sm font-medium transition-colors bg-white text-blue-600 shadow-sm hover:bg-gray-50 border border-gray-300 flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
title="立即刷新数据"
@click="refreshAllData()"
>
<i :class="['fas fa-sync-alt text-xs', { 'animate-spin': isRefreshing }]" />
<span>{{ isRefreshing ? '刷新中' : '刷新' }}</span>
</button>
</div>
</div> </div>
</div> </div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6"> <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- 饼图 --> <!-- 饼图 -->
<div class="card p-6"> <div class="card p-6">
<h4 class="text-lg font-semibold text-gray-800 mb-4">Token使用分布</h4> <h4 class="text-lg font-semibold text-gray-800 mb-4">
<div class="relative" style="height: 300px;"> Token使用分布
<canvas ref="modelUsageChart"></canvas> </h4>
<div
class="relative"
style="height: 300px;"
>
<canvas ref="modelUsageChart" />
</div> </div>
</div> </div>
<!-- 详细数据表格 --> <!-- 详细数据表格 -->
<div class="card p-6"> <div class="card p-6">
<h4 class="text-lg font-semibold text-gray-800 mb-4">详细统计数据</h4> <h4 class="text-lg font-semibold text-gray-800 mb-4">
<div v-if="dashboardModelStats.length === 0" class="text-center py-8"> 详细统计数据
<p class="text-gray-500">暂无模型使用数据</p> </h4>
<div
v-if="dashboardModelStats.length === 0"
class="text-center py-8"
>
<p class="text-gray-500">
暂无模型使用数据
</p>
</div> </div>
<div v-else class="overflow-auto max-h-[300px]"> <div
v-else
class="overflow-auto max-h-[300px]"
>
<table class="min-w-full"> <table class="min-w-full">
<thead class="bg-gray-50 sticky top-0"> <thead class="bg-gray-50 sticky top-0">
<tr> <tr>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-700">模型</th> <th class="px-4 py-2 text-left text-xs font-medium text-gray-700">
<th class="px-4 py-2 text-right text-xs font-medium text-gray-700">请求数</th> 模型
<th class="px-4 py-2 text-right text-xs font-medium text-gray-700">总Token</th> </th>
<th class="px-4 py-2 text-right text-xs font-medium text-gray-700">费用</th> <th class="px-4 py-2 text-right text-xs font-medium text-gray-700">
<th class="px-4 py-2 text-right text-xs font-medium text-gray-700">占比</th> 请求数
</th>
<th class="px-4 py-2 text-right text-xs font-medium text-gray-700">
总Token
</th>
<th class="px-4 py-2 text-right text-xs font-medium text-gray-700">
费用
</th>
<th class="px-4 py-2 text-right text-xs font-medium text-gray-700">
占比
</th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-gray-200"> <tbody class="divide-y divide-gray-200">
<tr v-for="stat in dashboardModelStats" :key="stat.model" class="hover:bg-gray-50"> <tr
<td class="px-4 py-2 text-sm text-gray-900">{{ stat.model }}</td> v-for="stat in dashboardModelStats"
<td class="px-4 py-2 text-sm text-gray-600 text-right">{{ formatNumber(stat.requests) }}</td> :key="stat.model"
<td class="px-4 py-2 text-sm text-gray-600 text-right">{{ formatNumber(stat.allTokens) }}</td> class="hover:bg-gray-50"
<td class="px-4 py-2 text-sm text-green-600 text-right font-medium">{{ stat.formatted ? stat.formatted.total : '$0.000000' }}</td> >
<td class="px-4 py-2 text-sm text-gray-900">
{{ stat.model }}
</td>
<td class="px-4 py-2 text-sm text-gray-600 text-right">
{{ formatNumber(stat.requests) }}
</td>
<td class="px-4 py-2 text-sm text-gray-600 text-right">
{{ formatNumber(stat.allTokens) }}
</td>
<td class="px-4 py-2 text-sm text-green-600 text-right font-medium">
{{ stat.formatted ? stat.formatted.total : '$0.000000' }}
</td>
<td class="px-4 py-2 text-sm font-medium text-right"> <td class="px-4 py-2 text-sm font-medium text-right">
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800"> <span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
{{ calculatePercentage(stat.allTokens, dashboardModelStats) }}% {{ calculatePercentage(stat.allTokens, dashboardModelStats) }}%
@@ -321,7 +407,7 @@
<div class="mb-8"> <div class="mb-8">
<div class="card p-6"> <div class="card p-6">
<div style="height: 300px;"> <div style="height: 300px;">
<canvas ref="usageTrendChart"></canvas> <canvas ref="usageTrendChart" />
</div> </div>
</div> </div>
</div> </div>
@@ -330,30 +416,32 @@
<div class="mb-8"> <div class="mb-8">
<div class="card p-6"> <div class="card p-6">
<div class="flex items-center justify-between mb-4"> <div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-900">API Keys 使用趋势</h3> <h3 class="text-lg font-semibold text-gray-900">
API Keys 使用趋势
</h3>
<!-- 维度切换按钮 --> <!-- 维度切换按钮 -->
<div class="flex gap-1 bg-gray-100 rounded-lg p-1"> <div class="flex gap-1 bg-gray-100 rounded-lg p-1">
<button <button
@click="apiKeysTrendMetric = 'requests'; updateApiKeysUsageTrendChart()"
:class="[ :class="[
'px-3 py-1 rounded-md text-sm font-medium transition-colors', 'px-3 py-1 rounded-md text-sm font-medium transition-colors',
apiKeysTrendMetric === 'requests' apiKeysTrendMetric === 'requests'
? 'bg-white text-blue-600 shadow-sm' ? 'bg-white text-blue-600 shadow-sm'
: 'text-gray-600 hover:text-gray-900' : 'text-gray-600 hover:text-gray-900'
]" ]"
@click="apiKeysTrendMetric = 'requests'; updateApiKeysUsageTrendChart()"
> >
<i class="fas fa-exchange-alt mr-1"></i>请求次数 <i class="fas fa-exchange-alt mr-1" />请求次数
</button> </button>
<button <button
@click="apiKeysTrendMetric = 'tokens'; updateApiKeysUsageTrendChart()"
:class="[ :class="[
'px-3 py-1 rounded-md text-sm font-medium transition-colors', 'px-3 py-1 rounded-md text-sm font-medium transition-colors',
apiKeysTrendMetric === 'tokens' apiKeysTrendMetric === 'tokens'
? 'bg-white text-blue-600 shadow-sm' ? 'bg-white text-blue-600 shadow-sm'
: 'text-gray-600 hover:text-gray-900' : 'text-gray-600 hover:text-gray-900'
]" ]"
@click="apiKeysTrendMetric = 'tokens'; updateApiKeysUsageTrendChart()"
> >
<i class="fas fa-coins mr-1"></i>Token 数量 <i class="fas fa-coins mr-1" />Token 数量
</button> </button>
</div> </div>
</div> </div>
@@ -366,7 +454,7 @@
</span> </span>
</div> </div>
<div style="height: 350px;"> <div style="height: 350px;">
<canvas ref="apiKeysUsageTrendChart"></canvas> <canvas ref="apiKeysUsageTrendChart" />
</div> </div>
</div> </div>
</div> </div>
@@ -395,8 +483,6 @@ const {
const { const {
loadDashboardData, loadDashboardData,
loadUsageTrend,
loadModelStats,
loadApiKeysTrend, loadApiKeysTrend,
setDateFilterPreset, setDateFilterPreset,
onCustomDateRangeChange, onCustomDateRangeChange,

View File

@@ -5,23 +5,41 @@
<!-- 使用自定义布局来保持登录页面的居中大logo样式 --> <!-- 使用自定义布局来保持登录页面的居中大logo样式 -->
<div class="w-20 h-20 mx-auto mb-6 bg-gradient-to-br from-blue-500/20 to-purple-500/20 border border-gray-300/30 rounded-2xl flex items-center justify-center backdrop-blur-sm overflow-hidden"> <div class="w-20 h-20 mx-auto mb-6 bg-gradient-to-br from-blue-500/20 to-purple-500/20 border border-gray-300/30 rounded-2xl flex items-center justify-center backdrop-blur-sm overflow-hidden">
<template v-if="!oemLoading"> <template v-if="!oemLoading">
<img v-if="authStore.oemSettings.siteIconData || authStore.oemSettings.siteIcon" <img
:src="authStore.oemSettings.siteIconData || authStore.oemSettings.siteIcon" v-if="authStore.oemSettings.siteIconData || authStore.oemSettings.siteIcon"
alt="Logo" :src="authStore.oemSettings.siteIconData || authStore.oemSettings.siteIcon"
class="w-12 h-12 object-contain" alt="Logo"
@error="(e) => e.target.style.display = 'none'"> class="w-12 h-12 object-contain"
<i v-else class="fas fa-cloud text-3xl text-gray-700"></i> @error="(e) => e.target.style.display = 'none'"
>
<i
v-else
class="fas fa-cloud text-3xl text-gray-700"
/>
</template> </template>
<div v-else class="w-12 h-12 bg-gray-300/50 rounded animate-pulse"></div> <div
v-else
class="w-12 h-12 bg-gray-300/50 rounded animate-pulse"
/>
</div> </div>
<template v-if="!oemLoading && authStore.oemSettings.siteName"> <template v-if="!oemLoading && authStore.oemSettings.siteName">
<h1 class="text-3xl font-bold text-white mb-2 header-title">{{ authStore.oemSettings.siteName }}</h1> <h1 class="text-3xl font-bold text-white mb-2 header-title">
{{ authStore.oemSettings.siteName }}
</h1>
</template> </template>
<div v-else-if="oemLoading" class="h-9 w-64 bg-gray-300/50 rounded animate-pulse mx-auto mb-2"></div> <div
<p class="text-gray-600 text-lg">管理后台</p> v-else-if="oemLoading"
class="h-9 w-64 bg-gray-300/50 rounded animate-pulse mx-auto mb-2"
/>
<p class="text-gray-600 text-lg">
管理后台
</p>
</div> </div>
<form @submit.prevent="handleLogin" class="space-y-6"> <form
class="space-y-6"
@submit.prevent="handleLogin"
>
<div> <div>
<label class="block text-sm font-semibold text-gray-900 mb-3">用户名</label> <label class="block text-sm font-semibold text-gray-900 mb-3">用户名</label>
<input <input
@@ -49,14 +67,23 @@
:disabled="authStore.loginLoading" :disabled="authStore.loginLoading"
class="btn btn-primary w-full py-4 px-6 text-lg font-semibold" class="btn btn-primary w-full py-4 px-6 text-lg font-semibold"
> >
<i v-if="!authStore.loginLoading" class="fas fa-sign-in-alt mr-2"></i> <i
<div v-if="authStore.loginLoading" class="loading-spinner mr-2"></div> v-if="!authStore.loginLoading"
class="fas fa-sign-in-alt mr-2"
/>
<div
v-if="authStore.loginLoading"
class="loading-spinner mr-2"
/>
{{ authStore.loginLoading ? '登录中...' : '登录' }} {{ authStore.loginLoading ? '登录中...' : '登录' }}
</button> </button>
</form> </form>
<div v-if="authStore.loginError" class="mt-6 p-4 bg-red-500/20 border border-red-500/30 rounded-xl text-red-800 text-sm text-center backdrop-blur-sm"> <div
<i class="fas fa-exclamation-triangle mr-2"></i>{{ authStore.loginError }} v-if="authStore.loginError"
class="mt-6 p-4 bg-red-500/20 border border-red-500/30 rounded-xl text-red-800 text-sm text-center backdrop-blur-sm"
>
<i class="fas fa-exclamation-triangle mr-2" />{{ authStore.loginError }}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -3,17 +3,29 @@
<div class="card p-6"> <div class="card p-6">
<div class="flex flex-col md:flex-row justify-between items-center gap-4 mb-6"> <div class="flex flex-col md:flex-row justify-between items-center gap-4 mb-6">
<div> <div>
<h3 class="text-xl font-bold text-gray-900 mb-2">其他设置</h3> <h3 class="text-xl font-bold text-gray-900 mb-2">
<p class="text-gray-600">自定义网站名称和图标</p> 其他设置
</h3>
<p class="text-gray-600">
自定义网站名称和图标
</p>
</div> </div>
</div> </div>
<div v-if="loading" class="text-center py-12"> <div
<div class="loading-spinner mx-auto mb-4"></div> v-if="loading"
<p class="text-gray-500">正在加载设置...</p> class="text-center py-12"
>
<div class="loading-spinner mx-auto mb-4" />
<p class="text-gray-500">
正在加载设置...
</p>
</div> </div>
<div v-else class="table-container"> <div
v-else
class="table-container"
>
<table class="min-w-full"> <table class="min-w-full">
<tbody class="divide-y divide-gray-200/50"> <tbody class="divide-y divide-gray-200/50">
<!-- 网站名称 --> <!-- 网站名称 -->
@@ -21,11 +33,15 @@
<td class="px-6 py-4 whitespace-nowrap w-48"> <td class="px-6 py-4 whitespace-nowrap w-48">
<div class="flex items-center"> <div class="flex items-center">
<div class="w-8 h-8 bg-gradient-to-br from-blue-500 to-blue-600 rounded-lg flex items-center justify-center mr-3"> <div class="w-8 h-8 bg-gradient-to-br from-blue-500 to-blue-600 rounded-lg flex items-center justify-center mr-3">
<i class="fas fa-font text-white text-xs"></i> <i class="fas fa-font text-white text-xs" />
</div> </div>
<div> <div>
<div class="text-sm font-semibold text-gray-900">网站名称</div> <div class="text-sm font-semibold text-gray-900">
<div class="text-xs text-gray-500">品牌标识</div> 网站名称
</div>
<div class="text-xs text-gray-500">
品牌标识
</div>
</div> </div>
</div> </div>
</td> </td>
@@ -37,7 +53,9 @@
placeholder="Claude Relay Service" placeholder="Claude Relay Service"
maxlength="100" maxlength="100"
> >
<p class="text-xs text-gray-500 mt-1">将显示在浏览器标题和页面头部</p> <p class="text-xs text-gray-500 mt-1">
将显示在浏览器标题和页面头部
</p>
</td> </td>
</tr> </tr>
@@ -46,18 +64,25 @@
<td class="px-6 py-4 whitespace-nowrap w-48"> <td class="px-6 py-4 whitespace-nowrap w-48">
<div class="flex items-center"> <div class="flex items-center">
<div class="w-8 h-8 bg-gradient-to-br from-purple-500 to-purple-600 rounded-lg flex items-center justify-center mr-3"> <div class="w-8 h-8 bg-gradient-to-br from-purple-500 to-purple-600 rounded-lg flex items-center justify-center mr-3">
<i class="fas fa-image text-white text-xs"></i> <i class="fas fa-image text-white text-xs" />
</div> </div>
<div> <div>
<div class="text-sm font-semibold text-gray-900">网站图标</div> <div class="text-sm font-semibold text-gray-900">
<div class="text-xs text-gray-500">Favicon</div> 网站图标
</div>
<div class="text-xs text-gray-500">
Favicon
</div>
</div> </div>
</div> </div>
</td> </td>
<td class="px-6 py-4"> <td class="px-6 py-4">
<div class="space-y-3"> <div class="space-y-3">
<!-- 图标预览 --> <!-- 图标预览 -->
<div v-if="oemSettings.siteIconData || oemSettings.siteIcon" class="inline-flex items-center gap-3 p-3 bg-gray-50 rounded-lg"> <div
v-if="oemSettings.siteIconData || oemSettings.siteIcon"
class="inline-flex items-center gap-3 p-3 bg-gray-50 rounded-lg"
>
<img <img
:src="oemSettings.siteIconData || oemSettings.siteIcon" :src="oemSettings.siteIconData || oemSettings.siteIcon"
alt="图标预览" alt="图标预览"
@@ -66,27 +91,27 @@
> >
<span class="text-sm text-gray-600">当前图标</span> <span class="text-sm text-gray-600">当前图标</span>
<button <button
@click="removeIcon"
class="text-red-600 hover:text-red-900 font-medium hover:bg-red-50 px-3 py-1 rounded-lg transition-colors" class="text-red-600 hover:text-red-900 font-medium hover:bg-red-50 px-3 py-1 rounded-lg transition-colors"
@click="removeIcon"
> >
<i class="fas fa-trash mr-1"></i>删除 <i class="fas fa-trash mr-1" />删除
</button> </button>
</div> </div>
<!-- 文件上传 --> <!-- 文件上传 -->
<div> <div>
<input <input
type="file" ref="iconFileInput"
ref="iconFileInput" type="file"
@change="handleIconUpload"
accept=".ico,.png,.jpg,.jpeg,.svg" accept=".ico,.png,.jpg,.jpeg,.svg"
class="hidden" class="hidden"
@change="handleIconUpload"
> >
<button <button
@click="$refs.iconFileInput.click()"
class="btn btn-success px-4 py-2" class="btn btn-success px-4 py-2"
@click="$refs.iconFileInput.click()"
> >
<i class="fas fa-upload mr-2"></i> <i class="fas fa-upload mr-2" />
上传图标 上传图标
</button> </button>
<span class="text-xs text-gray-500 ml-3">支持 .ico, .png, .jpg, .svg 格式最大 350KB</span> <span class="text-xs text-gray-500 ml-3">支持 .ico, .png, .jpg, .svg 格式最大 350KB</span>
@@ -97,32 +122,44 @@
<!-- 操作按钮 --> <!-- 操作按钮 -->
<tr> <tr>
<td class="px-6 py-6" colspan="2"> <td
class="px-6 py-6"
colspan="2"
>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="flex gap-3"> <div class="flex gap-3">
<button <button
@click="saveOemSettings"
:disabled="saving" :disabled="saving"
class="btn btn-primary px-6 py-3" class="btn btn-primary px-6 py-3"
:class="{ 'opacity-50 cursor-not-allowed': saving }" :class="{ 'opacity-50 cursor-not-allowed': saving }"
@click="saveOemSettings"
> >
<div v-if="saving" class="loading-spinner mr-2"></div> <div
<i v-else class="fas fa-save mr-2"></i> v-if="saving"
class="loading-spinner mr-2"
/>
<i
v-else
class="fas fa-save mr-2"
/>
{{ saving ? '保存中...' : '保存设置' }} {{ saving ? '保存中...' : '保存设置' }}
</button> </button>
<button <button
@click="resetOemSettings"
class="btn bg-gray-100 text-gray-700 hover:bg-gray-200 px-6 py-3" class="btn bg-gray-100 text-gray-700 hover:bg-gray-200 px-6 py-3"
:disabled="saving" :disabled="saving"
@click="resetOemSettings"
> >
<i class="fas fa-undo mr-2"></i> <i class="fas fa-undo mr-2" />
重置为默认 重置为默认
</button> </button>
</div> </div>
<div v-if="oemSettings.updatedAt" class="text-sm text-gray-500"> <div
<i class="fas fa-clock mr-1"></i> v-if="oemSettings.updatedAt"
class="text-sm text-gray-500"
>
<i class="fas fa-clock mr-1" />
最后更新{{ formatDateTime(oemSettings.updatedAt) }} 最后更新{{ formatDateTime(oemSettings.updatedAt) }}
</div> </div>
</div> </div>

File diff suppressed because it is too large Load Diff