mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 18:35:09 +00:00
refactor: standardize code formatting and linting configuration
- Replace .eslintrc.js with .eslintrc.cjs for better ES module compatibility - Add .prettierrc configuration for consistent code formatting - Update package.json with new lint and format scripts - Add nodemon.json for development hot reloading configuration - Standardize code formatting across all JavaScript and Vue files - Update web admin SPA with improved linting rules and formatting - Add prettier configuration to web admin SPA 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,122 +1,131 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div class="fixed inset-0 modal z-50 flex items-center justify-center p-4">
|
||||
<div class="modal-content w-full max-w-2xl p-8 mx-auto max-h-[90vh] overflow-y-auto custom-scrollbar">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div class="modal fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
<div
|
||||
class="modal-content custom-scrollbar mx-auto max-h-[90vh] w-full max-w-2xl overflow-y-auto p-8"
|
||||
>
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-12 h-12 bg-gradient-to-br from-green-500 to-green-600 rounded-xl flex items-center justify-center">
|
||||
<i class="fas fa-layer-group text-white text-lg" />
|
||||
<div
|
||||
class="flex h-12 w-12 items-center justify-center rounded-xl bg-gradient-to-br from-green-500 to-green-600"
|
||||
>
|
||||
<i class="fas fa-layer-group text-lg text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-xl font-bold text-gray-900">
|
||||
批量创建成功
|
||||
</h3>
|
||||
<p class="text-sm text-gray-600">
|
||||
成功创建 {{ apiKeys.length }} 个 API Key
|
||||
</p>
|
||||
<h3 class="text-xl font-bold text-gray-900">批量创建成功</h3>
|
||||
<p class="text-sm text-gray-600">成功创建 {{ apiKeys.length }} 个 API Key</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="text-gray-400 hover:text-gray-600 transition-colors"
|
||||
<button
|
||||
class="text-gray-400 transition-colors hover:text-gray-600"
|
||||
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="mb-6 border-l-4 border-amber-400 bg-amber-50 p-4">
|
||||
<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
|
||||
class="mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-lg bg-amber-400"
|
||||
>
|
||||
<i class="fas fa-exclamation-triangle text-sm text-white" />
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h5 class="font-semibold text-amber-900 mb-1">
|
||||
重要提醒
|
||||
</h5>
|
||||
<h5 class="mb-1 font-semibold text-amber-900">重要提醒</h5>
|
||||
<p class="text-sm text-amber-800">
|
||||
这是您唯一能看到所有 API Key 的机会。关闭此窗口后,系统将不再显示完整的 API Key。请立即下载并妥善保存。
|
||||
这是您唯一能看到所有 API Key 的机会。关闭此窗口后,系统将不再显示完整的 API
|
||||
Key。请立即下载并妥善保存。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
||||
<div class="bg-gradient-to-br from-blue-50 to-blue-100 rounded-lg p-4 border border-blue-200">
|
||||
<div class="mb-6 grid grid-cols-2 gap-4 md:grid-cols-4">
|
||||
<div
|
||||
class="rounded-lg border border-blue-200 bg-gradient-to-br from-blue-50 to-blue-100 p-4"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-xs text-blue-600 font-medium">
|
||||
创建数量
|
||||
</p>
|
||||
<p class="text-2xl font-bold text-blue-900 mt-1">
|
||||
<p class="text-xs font-medium text-blue-600">创建数量</p>
|
||||
<p class="mt-1 text-2xl font-bold text-blue-900">
|
||||
{{ apiKeys.length }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="w-10 h-10 bg-blue-500 bg-opacity-20 rounded-lg flex items-center justify-center">
|
||||
<div
|
||||
class="flex h-10 w-10 items-center justify-center rounded-lg bg-blue-500 bg-opacity-20"
|
||||
>
|
||||
<i class="fas fa-key text-blue-600" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gradient-to-br from-green-50 to-green-100 rounded-lg p-4 border border-green-200">
|
||||
|
||||
<div
|
||||
class="rounded-lg border border-green-200 bg-gradient-to-br from-green-50 to-green-100 p-4"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-xs text-green-600 font-medium">
|
||||
基础名称
|
||||
</p>
|
||||
<p class="text-lg font-bold text-green-900 mt-1 truncate">
|
||||
<p class="text-xs font-medium text-green-600">基础名称</p>
|
||||
<p class="mt-1 truncate text-lg font-bold text-green-900">
|
||||
{{ baseName }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="w-10 h-10 bg-green-500 bg-opacity-20 rounded-lg flex items-center justify-center">
|
||||
<div
|
||||
class="flex h-10 w-10 items-center justify-center rounded-lg bg-green-500 bg-opacity-20"
|
||||
>
|
||||
<i class="fas fa-tag text-green-600" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gradient-to-br from-purple-50 to-purple-100 rounded-lg p-4 border border-purple-200">
|
||||
|
||||
<div
|
||||
class="rounded-lg border border-purple-200 bg-gradient-to-br from-purple-50 to-purple-100 p-4"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-xs text-purple-600 font-medium">
|
||||
权限范围
|
||||
</p>
|
||||
<p class="text-lg font-bold text-purple-900 mt-1">
|
||||
<p class="text-xs font-medium text-purple-600">权限范围</p>
|
||||
<p class="mt-1 text-lg font-bold text-purple-900">
|
||||
{{ getPermissionText() }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="w-10 h-10 bg-purple-500 bg-opacity-20 rounded-lg flex items-center justify-center">
|
||||
<div
|
||||
class="flex h-10 w-10 items-center justify-center rounded-lg bg-purple-500 bg-opacity-20"
|
||||
>
|
||||
<i class="fas fa-shield-alt text-purple-600" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gradient-to-br from-orange-50 to-orange-100 rounded-lg p-4 border border-orange-200">
|
||||
|
||||
<div
|
||||
class="rounded-lg border border-orange-200 bg-gradient-to-br from-orange-50 to-orange-100 p-4"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-xs text-orange-600 font-medium">
|
||||
过期时间
|
||||
</p>
|
||||
<p class="text-lg font-bold text-orange-900 mt-1">
|
||||
<p class="text-xs font-medium text-orange-600">过期时间</p>
|
||||
<p class="mt-1 text-lg font-bold text-orange-900">
|
||||
{{ getExpiryText() }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="w-10 h-10 bg-orange-500 bg-opacity-20 rounded-lg flex items-center justify-center">
|
||||
<div
|
||||
class="flex h-10 w-10 items-center justify-center rounded-lg bg-orange-500 bg-opacity-20"
|
||||
>
|
||||
<i class="fas fa-clock text-orange-600" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- API Keys 预览 -->
|
||||
<div class="mb-6">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<label class="text-sm font-semibold text-gray-700">API Keys 预览</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
<button
|
||||
class="flex items-center gap-1 text-xs text-blue-600 hover:text-blue-800"
|
||||
type="button"
|
||||
class="text-xs text-blue-600 hover:text-blue-800 flex items-center gap-1"
|
||||
@click="togglePreview"
|
||||
>
|
||||
<i :class="['fas', showPreview ? 'fa-eye-slash' : 'fa-eye']" />
|
||||
@@ -125,35 +134,35 @@
|
||||
<span class="text-xs text-gray-500">(最多显示前10个)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div
|
||||
v-if="showPreview"
|
||||
class="bg-gray-900 rounded-lg p-4 max-h-48 overflow-y-auto custom-scrollbar"
|
||||
class="custom-scrollbar max-h-48 overflow-y-auto rounded-lg bg-gray-900 p-4"
|
||||
>
|
||||
<pre class="text-xs text-gray-300 font-mono">{{ getPreviewText() }}</pre>
|
||||
<pre class="font-mono text-xs text-gray-300">{{ getPreviewText() }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="flex gap-3">
|
||||
<button
|
||||
class="flex-1 btn btn-primary py-3 px-6 font-semibold flex items-center justify-center gap-2"
|
||||
<button
|
||||
class="btn btn-primary flex flex-1 items-center justify-center gap-2 px-6 py-3 font-semibold"
|
||||
@click="downloadApiKeys"
|
||||
>
|
||||
<i class="fas fa-download" />
|
||||
下载所有 API Keys
|
||||
</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"
|
||||
<button
|
||||
class="rounded-xl border border-gray-300 bg-gray-200 px-6 py-3 font-semibold text-gray-800 transition-colors hover:bg-gray-300"
|
||||
@click="handleClose"
|
||||
>
|
||||
我已保存
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 额外提示 -->
|
||||
<div class="mt-4 p-3 bg-blue-50 rounded-lg border border-blue-200">
|
||||
<p class="text-xs text-blue-700 flex items-start">
|
||||
<div class="mt-4 rounded-lg border border-blue-200 bg-blue-50 p-3">
|
||||
<p class="flex items-start text-xs text-blue-700">
|
||||
<i class="fas fa-info-circle mr-2 mt-0.5 flex-shrink-0" />
|
||||
<span>
|
||||
下载的文件格式为文本文件(.txt),每行包含一个 API Key。
|
||||
@@ -197,9 +206,9 @@ const getPermissionText = () => {
|
||||
if (props.apiKeys.length === 0) return '未知'
|
||||
const permissions = props.apiKeys[0].permissions
|
||||
const permissionMap = {
|
||||
'all': '全部服务',
|
||||
'claude': '仅 Claude',
|
||||
'gemini': '仅 Gemini'
|
||||
all: '全部服务',
|
||||
claude: '仅 Claude',
|
||||
gemini: '仅 Gemini'
|
||||
}
|
||||
return permissionMap[permissions] || permissions
|
||||
}
|
||||
@@ -209,11 +218,11 @@ const getExpiryText = () => {
|
||||
if (props.apiKeys.length === 0) return '未知'
|
||||
const expiresAt = props.apiKeys[0].expiresAt
|
||||
if (!expiresAt) return '永不过期'
|
||||
|
||||
|
||||
const expiryDate = new Date(expiresAt)
|
||||
const now = new Date()
|
||||
const diffDays = Math.ceil((expiryDate - now) / (1000 * 60 * 60 * 24))
|
||||
|
||||
|
||||
if (diffDays <= 7) return `${diffDays}天`
|
||||
if (diffDays <= 30) return `${Math.ceil(diffDays / 7)}周`
|
||||
if (diffDays <= 365) return `${Math.ceil(diffDays / 30)}个月`
|
||||
@@ -228,44 +237,46 @@ const togglePreview = () => {
|
||||
// 获取预览文本
|
||||
const getPreviewText = () => {
|
||||
const previewKeys = props.apiKeys.slice(0, 10)
|
||||
const lines = previewKeys.map((key, index) => {
|
||||
const lines = previewKeys.map((key) => {
|
||||
return `${key.name}: ${key.apiKey || key.key || ''}`
|
||||
})
|
||||
|
||||
|
||||
if (props.apiKeys.length > 10) {
|
||||
lines.push(`... 还有 ${props.apiKeys.length - 10} 个 API Key`)
|
||||
}
|
||||
|
||||
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
// 下载 API Keys
|
||||
const downloadApiKeys = () => {
|
||||
// 生成文件内容
|
||||
const content = props.apiKeys.map(key => {
|
||||
return `${key.name}: ${key.apiKey || key.key || ''}`
|
||||
}).join('\n')
|
||||
|
||||
const content = props.apiKeys
|
||||
.map((key) => {
|
||||
return `${key.name}: ${key.apiKey || key.key || ''}`
|
||||
})
|
||||
.join('\n')
|
||||
|
||||
// 创建 Blob 对象
|
||||
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' })
|
||||
|
||||
|
||||
// 创建下载链接
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
|
||||
|
||||
// 生成文件名(包含时间戳)
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5)
|
||||
link.download = `api-keys-${baseName.value}-${timestamp}.txt`
|
||||
|
||||
|
||||
// 触发下载
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
|
||||
|
||||
// 释放 URL 对象
|
||||
URL.revokeObjectURL(url)
|
||||
|
||||
|
||||
showToast('API Keys 文件已下载', 'success')
|
||||
}
|
||||
|
||||
@@ -306,9 +317,7 @@ const handleDirectClose = async () => {
|
||||
}
|
||||
} else {
|
||||
// 降级方案
|
||||
const confirmed = confirm(
|
||||
'您还没有下载 API Keys,关闭后将无法再次查看。\n\n确定要关闭吗?'
|
||||
)
|
||||
const confirmed = confirm('您还没有下载 API Keys,关闭后将无法再次查看。\n\n确定要关闭吗?')
|
||||
if (confirmed) {
|
||||
emit('close')
|
||||
}
|
||||
@@ -321,4 +330,4 @@ pre {
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,60 +1,62 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div class="fixed inset-0 modal z-50 flex items-center justify-center p-3 sm:p-4">
|
||||
<div class="modal-content w-full max-w-4xl p-4 sm:p-6 md:p-8 mx-auto max-h-[90vh] flex flex-col">
|
||||
<div class="flex items-center justify-between mb-4 sm:mb-6">
|
||||
<div class="modal fixed inset-0 z-50 flex items-center justify-center p-3 sm:p-4">
|
||||
<div
|
||||
class="modal-content mx-auto flex max-h-[90vh] w-full max-w-4xl flex-col p-4 sm:p-6 md:p-8"
|
||||
>
|
||||
<div class="mb-4 flex items-center justify-between sm:mb-6">
|
||||
<div class="flex items-center gap-2 sm:gap-3">
|
||||
<div class="w-8 h-8 sm:w-10 sm:h-10 bg-gradient-to-br from-blue-500 to-blue-600 rounded-lg sm:rounded-xl flex items-center justify-center">
|
||||
<i class="fas fa-edit text-white text-sm sm:text-base" />
|
||||
<div
|
||||
class="flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-blue-500 to-blue-600 sm:h-10 sm:w-10 sm:rounded-xl"
|
||||
>
|
||||
<i class="fas fa-edit text-sm text-white sm:text-base" />
|
||||
</div>
|
||||
<h3 class="text-lg sm:text-xl font-bold text-gray-900">
|
||||
编辑 API Key
|
||||
</h3>
|
||||
<h3 class="text-lg font-bold text-gray-900 sm:text-xl">编辑 API Key</h3>
|
||||
</div>
|
||||
<button
|
||||
class="text-gray-400 hover:text-gray-600 transition-colors p-1"
|
||||
<button
|
||||
class="p-1 text-gray-400 transition-colors hover:text-gray-600"
|
||||
@click="$emit('close')"
|
||||
>
|
||||
<i class="fas fa-times text-lg sm:text-xl" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
<form
|
||||
class="space-y-4 sm:space-y-6 modal-scroll-content custom-scrollbar flex-1"
|
||||
class="modal-scroll-content custom-scrollbar flex-1 space-y-4 sm:space-y-6"
|
||||
@submit.prevent="updateApiKey"
|
||||
>
|
||||
<div>
|
||||
<label class="block text-xs sm:text-sm font-semibold text-gray-700 mb-1.5 sm:mb-3">名称</label>
|
||||
<input
|
||||
:value="form.name"
|
||||
type="text"
|
||||
disabled
|
||||
class="form-input w-full bg-gray-100 cursor-not-allowed text-sm"
|
||||
<label class="mb-1.5 block text-xs font-semibold text-gray-700 sm:mb-3 sm:text-sm"
|
||||
>名称</label
|
||||
>
|
||||
<p class="text-xs text-gray-500 mt-1 sm:mt-2">
|
||||
名称不可修改
|
||||
</p>
|
||||
<input
|
||||
class="form-input w-full cursor-not-allowed bg-gray-100 text-sm"
|
||||
disabled
|
||||
type="text"
|
||||
:value="form.name"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-gray-500 sm:mt-2">名称不可修改</p>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 标签 -->
|
||||
<div>
|
||||
<label class="block text-xs sm:text-sm font-semibold text-gray-700 mb-1.5 sm:mb-3">标签</label>
|
||||
<label class="mb-1.5 block text-xs font-semibold text-gray-700 sm:mb-3 sm:text-sm"
|
||||
>标签</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="mb-2 text-xs font-medium text-gray-600">已选择的标签:</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"
|
||||
:key="'selected-' + index"
|
||||
class="inline-flex items-center gap-1 rounded-full bg-blue-100 px-3 py-1 text-sm text-blue-800"
|
||||
>
|
||||
{{ tag }}
|
||||
<button
|
||||
class="ml-1 hover:text-blue-900"
|
||||
type="button"
|
||||
class="ml-1 hover:text-blue-900"
|
||||
@click="removeTag(index)"
|
||||
>
|
||||
<i class="fas fa-times text-xs" />
|
||||
@@ -62,428 +64,388 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 可选择的已有标签 -->
|
||||
<div v-if="unselectedTags.length > 0">
|
||||
<div class="text-xs font-medium text-gray-600 mb-2">
|
||||
点击选择已有标签:
|
||||
</div>
|
||||
<div class="mb-2 text-xs font-medium text-gray-600">点击选择已有标签:</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
v-for="tag in unselectedTags"
|
||||
:key="'available-' + tag"
|
||||
class="inline-flex items-center gap-1 rounded-full bg-gray-100 px-3 py-1 text-sm text-gray-700 transition-colors hover:bg-blue-100 hover:text-blue-700"
|
||||
type="button"
|
||||
class="inline-flex items-center gap-1 px-3 py-1 bg-gray-100 text-gray-700 text-sm rounded-full hover:bg-blue-100 hover:text-blue-700 transition-colors"
|
||||
@click="selectTag(tag)"
|
||||
>
|
||||
<i class="fas fa-tag text-gray-500 text-xs" />
|
||||
<i class="fas fa-tag text-xs text-gray-500" />
|
||||
{{ tag }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 创建新标签 -->
|
||||
<div>
|
||||
<div class="text-xs font-medium text-gray-600 mb-2">
|
||||
创建新标签:
|
||||
</div>
|
||||
<div class="mb-2 text-xs font-medium text-gray-600">创建新标签:</div>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
v-model="newTag"
|
||||
type="text"
|
||||
<input
|
||||
v-model="newTag"
|
||||
class="form-input flex-1"
|
||||
placeholder="输入新标签名称"
|
||||
type="text"
|
||||
@keypress.enter.prevent="addTag"
|
||||
>
|
||||
/>
|
||||
<button
|
||||
class="rounded-lg bg-green-500 px-4 py-2 text-white transition-colors hover:bg-green-600"
|
||||
type="button"
|
||||
class="px-4 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors"
|
||||
@click="addTag"
|
||||
>
|
||||
<i class="fas fa-plus" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-gray-500">
|
||||
用于标记不同团队或用途,方便筛选管理
|
||||
</p>
|
||||
|
||||
<p class="text-xs text-gray-500">用于标记不同团队或用途,方便筛选管理</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 速率限制设置 -->
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-lg p-3">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<div class="w-6 h-6 bg-blue-500 rounded flex items-center justify-center flex-shrink-0">
|
||||
<i class="fas fa-tachometer-alt text-white text-xs" />
|
||||
<div class="rounded-lg border border-blue-200 bg-blue-50 p-3">
|
||||
<div class="mb-2 flex items-center gap-2">
|
||||
<div
|
||||
class="flex h-6 w-6 flex-shrink-0 items-center justify-center rounded bg-blue-500"
|
||||
>
|
||||
<i class="fas fa-tachometer-alt text-xs text-white" />
|
||||
</div>
|
||||
<h4 class="font-semibold text-gray-800 text-sm">
|
||||
速率限制设置 (可选)
|
||||
</h4>
|
||||
<h4 class="text-sm font-semibold text-gray-800">速率限制设置 (可选)</h4>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="space-y-2">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-2">
|
||||
<div class="grid grid-cols-1 gap-2 lg:grid-cols-3">
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">时间窗口 (分钟)</label>
|
||||
<input
|
||||
v-model="form.rateLimitWindow"
|
||||
type="number"
|
||||
<label class="mb-1 block text-xs font-medium text-gray-700"
|
||||
>时间窗口 (分钟)</label
|
||||
>
|
||||
<input
|
||||
v-model="form.rateLimitWindow"
|
||||
class="form-input w-full text-sm"
|
||||
min="1"
|
||||
placeholder="无限制"
|
||||
class="form-input w-full text-sm"
|
||||
>
|
||||
<p class="text-xs text-gray-500 mt-0.5 ml-2">
|
||||
时间段单位
|
||||
</p>
|
||||
placeholder="无限制"
|
||||
type="number"
|
||||
/>
|
||||
<p class="ml-2 mt-0.5 text-xs text-gray-500">时间段单位</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">请求次数限制</label>
|
||||
<input
|
||||
v-model="form.rateLimitRequests"
|
||||
type="number"
|
||||
<label class="mb-1 block text-xs font-medium text-gray-700">请求次数限制</label>
|
||||
<input
|
||||
v-model="form.rateLimitRequests"
|
||||
class="form-input w-full text-sm"
|
||||
min="1"
|
||||
placeholder="无限制"
|
||||
class="form-input w-full text-sm"
|
||||
>
|
||||
<p class="text-xs text-gray-500 mt-0.5 ml-2">
|
||||
窗口内最大请求
|
||||
</p>
|
||||
placeholder="无限制"
|
||||
type="number"
|
||||
/>
|
||||
<p class="ml-2 mt-0.5 text-xs text-gray-500">窗口内最大请求</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="无限制"
|
||||
<label class="mb-1 block text-xs font-medium text-gray-700">Token 限制</label>
|
||||
<input
|
||||
v-model="form.tokenLimit"
|
||||
class="form-input w-full text-sm"
|
||||
>
|
||||
<p class="text-xs text-gray-500 mt-0.5 ml-2">
|
||||
窗口内最大Token
|
||||
</p>
|
||||
placeholder="无限制"
|
||||
type="number"
|
||||
/>
|
||||
<p class="ml-2 mt-0.5 text-xs text-gray-500">窗口内最大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> 时间窗口=1,Token=10000 → 每分钟最多10,000个Token</div>
|
||||
<div><strong>示例3:</strong> 窗口=30,请求=50,Token=100000 → 每30分钟50次请求且不超10万Token</div>
|
||||
<div class="rounded-lg bg-blue-100 p-2">
|
||||
<h5 class="mb-1 text-xs font-semibold text-blue-800">💡 使用示例</h5>
|
||||
<div class="space-y-0.5 text-xs text-blue-700">
|
||||
<div>
|
||||
<strong>示例1:</strong> 时间窗口=60,请求次数=1000 → 每60分钟最多1000次请求
|
||||
</div>
|
||||
<div>
|
||||
<strong>示例2:</strong> 时间窗口=1,Token=10000 → 每分钟最多10,000个Token
|
||||
</div>
|
||||
<div>
|
||||
<strong>示例3:</strong> 窗口=30,请求=50,Token=100000 →
|
||||
每30分钟50次请求且不超10万Token
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-3">每日费用限制 (美元)</label>
|
||||
<label class="mb-3 block text-sm font-semibold text-gray-700"
|
||||
>每日费用限制 (美元)</label
|
||||
>
|
||||
<div class="space-y-3">
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
class="rounded-lg bg-gray-100 px-3 py-1 text-sm font-medium hover:bg-gray-200"
|
||||
type="button"
|
||||
class="px-3 py-1 bg-gray-100 hover:bg-gray-200 rounded-lg text-sm font-medium"
|
||||
@click="form.dailyCostLimit = '50'"
|
||||
>
|
||||
$50
|
||||
</button>
|
||||
<button
|
||||
class="rounded-lg bg-gray-100 px-3 py-1 text-sm font-medium hover:bg-gray-200"
|
||||
type="button"
|
||||
class="px-3 py-1 bg-gray-100 hover:bg-gray-200 rounded-lg text-sm font-medium"
|
||||
@click="form.dailyCostLimit = '100'"
|
||||
>
|
||||
$100
|
||||
</button>
|
||||
<button
|
||||
class="rounded-lg bg-gray-100 px-3 py-1 text-sm font-medium hover:bg-gray-200"
|
||||
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
|
||||
class="rounded-lg bg-gray-100 px-3 py-1 text-sm font-medium hover:bg-gray-200"
|
||||
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 表示无限制"
|
||||
<input
|
||||
v-model="form.dailyCostLimit"
|
||||
class="form-input w-full"
|
||||
>
|
||||
min="0"
|
||||
placeholder="0 表示无限制"
|
||||
step="0.01"
|
||||
type="number"
|
||||
/>
|
||||
<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 表示无限制"
|
||||
<label class="mb-3 block text-sm font-semibold text-gray-700">并发限制</label>
|
||||
<input
|
||||
v-model="form.concurrencyLimit"
|
||||
class="form-input w-full"
|
||||
>
|
||||
<p class="text-xs text-gray-500 mt-2">
|
||||
设置此 API Key 可同时处理的最大请求数
|
||||
</p>
|
||||
min="0"
|
||||
placeholder="0 表示无限制"
|
||||
type="number"
|
||||
/>
|
||||
<p class="mt-2 text-xs text-gray-500">设置此 API Key 可同时处理的最大请求数</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-3">服务权限</label>
|
||||
<label class="mb-3 block text-sm font-semibold text-gray-700">服务权限</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"
|
||||
>
|
||||
<label class="flex cursor-pointer items-center">
|
||||
<input v-model="form.permissions" class="mr-2" type="radio" value="all" />
|
||||
<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"
|
||||
>
|
||||
<label class="flex cursor-pointer items-center">
|
||||
<input v-model="form.permissions" class="mr-2" type="radio" value="claude" />
|
||||
<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"
|
||||
>
|
||||
<label class="flex cursor-pointer items-center">
|
||||
<input v-model="form.permissions" class="mr-2" type="radio" value="gemini" />
|
||||
<span class="text-sm text-gray-700">仅 Gemini</span>
|
||||
</label>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 mt-2">
|
||||
控制此 API Key 可以访问哪些服务
|
||||
</p>
|
||||
<p class="mt-2 text-xs text-gray-500">控制此 API Key 可以访问哪些服务</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<label class="text-sm font-semibold text-gray-700">专属账号绑定</label>
|
||||
<button
|
||||
type="button"
|
||||
class="text-blue-600 hover:text-blue-800 text-sm flex items-center gap-1 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
title="刷新账号列表"
|
||||
class="flex items-center gap-1 text-sm text-blue-600 transition-colors hover:text-blue-800 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
:disabled="accountsLoading"
|
||||
title="刷新账号列表"
|
||||
type="button"
|
||||
@click="refreshAccounts"
|
||||
>
|
||||
<i :class="['fas', accountsLoading ? 'fa-spinner fa-spin' : 'fa-sync-alt', 'text-xs']" />
|
||||
<i
|
||||
:class="[
|
||||
'fas',
|
||||
accountsLoading ? 'fa-spinner fa-spin' : 'fa-sync-alt',
|
||||
'text-xs'
|
||||
]"
|
||||
/>
|
||||
<span>{{ accountsLoading ? '刷新中...' : '刷新账号' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-3">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-600 mb-1">Claude 专属账号</label>
|
||||
<label class="mb-1 block text-sm font-medium text-gray-600">Claude 专属账号</label>
|
||||
<AccountSelector
|
||||
v-model="form.claudeAccountId"
|
||||
platform="claude"
|
||||
:accounts="localAccounts.claude"
|
||||
:groups="localAccounts.claudeGroups"
|
||||
:disabled="form.permissions === 'gemini'"
|
||||
placeholder="请选择Claude账号"
|
||||
default-option-text="使用共享账号池"
|
||||
:disabled="form.permissions === 'gemini'"
|
||||
:groups="localAccounts.claudeGroups"
|
||||
placeholder="请选择Claude账号"
|
||||
platform="claude"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-600 mb-1">Gemini 专属账号</label>
|
||||
<label class="mb-1 block text-sm font-medium text-gray-600">Gemini 专属账号</label>
|
||||
<AccountSelector
|
||||
v-model="form.geminiAccountId"
|
||||
platform="gemini"
|
||||
:accounts="localAccounts.gemini"
|
||||
:groups="localAccounts.geminiGroups"
|
||||
:disabled="form.permissions === 'claude'"
|
||||
placeholder="请选择Gemini账号"
|
||||
default-option-text="使用共享账号池"
|
||||
:disabled="form.permissions === 'claude'"
|
||||
:groups="localAccounts.geminiGroups"
|
||||
placeholder="请选择Gemini账号"
|
||||
platform="gemini"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 mt-2">
|
||||
修改绑定账号将影响此API Key的请求路由
|
||||
</p>
|
||||
<p class="mt-2 text-xs text-gray-500">修改绑定账号将影响此API Key的请求路由</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<div class="flex items-center mb-3">
|
||||
<input
|
||||
id="editEnableModelRestriction"
|
||||
v-model="form.enableModelRestriction"
|
||||
<div class="mb-3 flex items-center">
|
||||
<input
|
||||
id="editEnableModelRestriction"
|
||||
v-model="form.enableModelRestriction"
|
||||
class="h-4 w-4 rounded border-gray-300 bg-gray-100 text-blue-600 focus:ring-blue-500"
|
||||
type="checkbox"
|
||||
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
|
||||
>
|
||||
/>
|
||||
<label
|
||||
class="ml-2 cursor-pointer text-sm font-semibold text-gray-700"
|
||||
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 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"
|
||||
<label class="mb-2 block text-sm font-medium text-gray-600">限制的模型列表</label>
|
||||
<div
|
||||
class="mb-3 flex min-h-[32px] flex-wrap gap-2 rounded-lg border border-gray-200 bg-gray-50 p-2"
|
||||
>
|
||||
<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"
|
||||
class="inline-flex items-center rounded-full bg-red-100 px-3 py-1 text-sm text-red-800"
|
||||
>
|
||||
{{ model }}
|
||||
<button
|
||||
type="button"
|
||||
<button
|
||||
class="ml-2 text-red-600 hover:text-red-800"
|
||||
type="button"
|
||||
@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 v-if="form.restrictedModels.length === 0" class="text-sm text-gray-400">
|
||||
暂无限制的模型
|
||||
</span>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<!-- 快速添加按钮 -->
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
v-for="model in availableQuickModels"
|
||||
<button
|
||||
v-for="model in availableQuickModels"
|
||||
:key="model"
|
||||
class="flex-shrink-0 rounded-lg bg-gray-100 px-3 py-1 text-xs text-gray-700 transition-colors hover:bg-gray-200 sm:text-sm"
|
||||
type="button"
|
||||
class="px-3 py-1 text-xs sm:text-sm bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors flex-shrink-0"
|
||||
@click="quickAddRestrictedModel(model)"
|
||||
>
|
||||
{{ model }}
|
||||
</button>
|
||||
<span
|
||||
v-if="availableQuickModels.length === 0"
|
||||
class="text-gray-400 text-sm italic"
|
||||
class="text-sm italic text-gray-400"
|
||||
>
|
||||
所有常用模型已在限制列表中
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 手动输入 -->
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
<input
|
||||
v-model="form.modelInput"
|
||||
type="text"
|
||||
placeholder="输入模型名称,按回车添加"
|
||||
class="form-input flex-1"
|
||||
placeholder="输入模型名称,按回车添加"
|
||||
type="text"
|
||||
@keydown.enter.prevent="addRestrictedModel"
|
||||
>
|
||||
<button
|
||||
/>
|
||||
<button
|
||||
class="rounded-lg bg-red-500 px-4 py-2 text-white transition-colors hover:bg-red-600"
|
||||
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>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 mt-2">
|
||||
<p class="mt-2 text-xs text-gray-500">
|
||||
设置此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"
|
||||
<div class="mb-3 flex items-center">
|
||||
<input
|
||||
id="editEnableClientRestriction"
|
||||
v-model="form.enableClientRestriction"
|
||||
class="h-4 w-4 rounded border-gray-300 bg-gray-100 text-blue-600 focus:ring-blue-500"
|
||||
type="checkbox"
|
||||
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
|
||||
>
|
||||
/>
|
||||
<label
|
||||
class="ml-2 cursor-pointer text-sm font-semibold text-gray-700"
|
||||
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 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>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-600">允许的客户端</label>
|
||||
<p class="mb-3 text-xs text-gray-500">勾选允许使用此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}`"
|
||||
<div v-for="client in supportedClients" :key="client.id" class="flex items-start">
|
||||
<input
|
||||
:id="`edit_client_${client.id}`"
|
||||
v-model="form.allowedClients"
|
||||
class="mt-0.5 h-4 w-4 rounded border-gray-300 bg-gray-100 text-blue-600 focus:ring-blue-500"
|
||||
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"
|
||||
>
|
||||
/>
|
||||
<label class="ml-2 flex-1 cursor-pointer" :for="`edit_client_${client.id}`">
|
||||
<span class="text-sm font-medium text-gray-700">{{ client.name }}</span>
|
||||
<span class="text-xs text-gray-500 block">{{ client.description }}</span>
|
||||
<span class="block text-xs text-gray-500">{{ 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"
|
||||
<button
|
||||
class="flex-1 rounded-xl bg-gray-100 px-6 py-3 font-semibold text-gray-700 transition-colors hover:bg-gray-200"
|
||||
type="button"
|
||||
@click="$emit('close')"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
<button
|
||||
class="btn btn-primary flex-1 px-6 py-3 font-semibold"
|
||||
:disabled="loading"
|
||||
class="btn btn-primary flex-1 py-3 px-6 font-semibold"
|
||||
type="submit"
|
||||
>
|
||||
<div
|
||||
v-if="loading"
|
||||
class="loading-spinner mr-2"
|
||||
/>
|
||||
<i
|
||||
v-else
|
||||
class="fas fa-save mr-2"
|
||||
/>
|
||||
<div v-if="loading" class="loading-spinner mr-2" />
|
||||
<i v-else class="fas fa-save mr-2" />
|
||||
{{ loading ? '保存中...' : '保存修改' }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -496,7 +458,6 @@
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { showToast } from '@/utils/toast'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useClientsStore } from '@/stores/clients'
|
||||
import { useApiKeysStore } from '@/stores/apiKeys'
|
||||
import { apiClient } from '@/config/api'
|
||||
@@ -515,7 +476,7 @@ const props = defineProps({
|
||||
|
||||
const emit = defineEmits(['close', 'success'])
|
||||
|
||||
const authStore = useAuthStore()
|
||||
// const authStore = useAuthStore()
|
||||
const clientsStore = useClientsStore()
|
||||
const apiKeysStore = useApiKeysStore()
|
||||
const loading = ref(false)
|
||||
@@ -531,7 +492,7 @@ const availableTags = ref([])
|
||||
|
||||
// 计算未选择的标签
|
||||
const unselectedTags = computed(() => {
|
||||
return availableTags.value.filter(tag => !form.tags.includes(tag))
|
||||
return availableTags.value.filter((tag) => !form.tags.includes(tag))
|
||||
})
|
||||
|
||||
// 表单数据
|
||||
@@ -553,7 +514,6 @@ const form = reactive({
|
||||
tags: []
|
||||
})
|
||||
|
||||
|
||||
// 添加限制的模型
|
||||
const addRestrictedModel = () => {
|
||||
if (form.modelInput && !form.restrictedModels.includes(form.modelInput)) {
|
||||
@@ -568,14 +528,11 @@ const removeRestrictedModel = (index) => {
|
||||
}
|
||||
|
||||
// 常用模型列表
|
||||
const commonModels = ref([
|
||||
'claude-opus-4-20250514',
|
||||
'claude-opus-4-1-20250805'
|
||||
])
|
||||
const commonModels = ref(['claude-opus-4-20250514', 'claude-opus-4-1-20250805'])
|
||||
|
||||
// 可用的快捷模型(过滤掉已在限制列表中的)
|
||||
const availableQuickModels = computed(() => {
|
||||
return commonModels.value.filter(model => !form.restrictedModels.includes(model))
|
||||
return commonModels.value.filter((model) => !form.restrictedModels.includes(model))
|
||||
})
|
||||
|
||||
// 快速添加限制的模型
|
||||
@@ -609,57 +566,70 @@ const removeTag = (index) => {
|
||||
// 更新 API Key
|
||||
const updateApiKey = async () => {
|
||||
loading.value = true
|
||||
|
||||
|
||||
try {
|
||||
// 准备提交的数据
|
||||
const data = {
|
||||
tokenLimit: form.tokenLimit !== '' && form.tokenLimit !== null ? parseInt(form.tokenLimit) : 0,
|
||||
rateLimitWindow: form.rateLimitWindow !== '' && form.rateLimitWindow !== null ? parseInt(form.rateLimitWindow) : 0,
|
||||
rateLimitRequests: form.rateLimitRequests !== '' && form.rateLimitRequests !== null ? parseInt(form.rateLimitRequests) : 0,
|
||||
concurrencyLimit: form.concurrencyLimit !== '' && form.concurrencyLimit !== null ? parseInt(form.concurrencyLimit) : 0,
|
||||
dailyCostLimit: form.dailyCostLimit !== '' && form.dailyCostLimit !== null ? parseFloat(form.dailyCostLimit) : 0,
|
||||
tokenLimit:
|
||||
form.tokenLimit !== '' && form.tokenLimit !== null ? parseInt(form.tokenLimit) : 0,
|
||||
rateLimitWindow:
|
||||
form.rateLimitWindow !== '' && form.rateLimitWindow !== null
|
||||
? parseInt(form.rateLimitWindow)
|
||||
: 0,
|
||||
rateLimitRequests:
|
||||
form.rateLimitRequests !== '' && form.rateLimitRequests !== null
|
||||
? parseInt(form.rateLimitRequests)
|
||||
: 0,
|
||||
concurrencyLimit:
|
||||
form.concurrencyLimit !== '' && form.concurrencyLimit !== null
|
||||
? parseInt(form.concurrencyLimit)
|
||||
: 0,
|
||||
dailyCostLimit:
|
||||
form.dailyCostLimit !== '' && form.dailyCostLimit !== null
|
||||
? parseFloat(form.dailyCostLimit)
|
||||
: 0,
|
||||
permissions: form.permissions,
|
||||
tags: form.tags
|
||||
}
|
||||
|
||||
|
||||
// 处理Claude账户绑定(区分OAuth和Console)
|
||||
if (form.claudeAccountId) {
|
||||
if (form.claudeAccountId.startsWith('console:')) {
|
||||
// Claude Console账户
|
||||
data.claudeConsoleAccountId = form.claudeAccountId.substring(8);
|
||||
data.claudeAccountId = null; // 清空OAuth账号
|
||||
data.claudeConsoleAccountId = form.claudeAccountId.substring(8)
|
||||
data.claudeAccountId = null // 清空OAuth账号
|
||||
} else if (!form.claudeAccountId.startsWith('group:')) {
|
||||
// Claude OAuth账户(非分组)
|
||||
data.claudeAccountId = form.claudeAccountId;
|
||||
data.claudeConsoleAccountId = null; // 清空Console账号
|
||||
data.claudeAccountId = form.claudeAccountId
|
||||
data.claudeConsoleAccountId = null // 清空Console账号
|
||||
} else {
|
||||
// 分组
|
||||
data.claudeAccountId = form.claudeAccountId;
|
||||
data.claudeConsoleAccountId = null; // 清空Console账号
|
||||
data.claudeAccountId = form.claudeAccountId
|
||||
data.claudeConsoleAccountId = null // 清空Console账号
|
||||
}
|
||||
} else {
|
||||
// 使用共享池,清空所有绑定
|
||||
data.claudeAccountId = null;
|
||||
data.claudeConsoleAccountId = null;
|
||||
data.claudeAccountId = null
|
||||
data.claudeConsoleAccountId = null
|
||||
}
|
||||
|
||||
|
||||
// Gemini账户绑定
|
||||
if (form.geminiAccountId) {
|
||||
data.geminiAccountId = form.geminiAccountId;
|
||||
data.geminiAccountId = form.geminiAccountId
|
||||
} else {
|
||||
data.geminiAccountId = null;
|
||||
data.geminiAccountId = null
|
||||
}
|
||||
|
||||
|
||||
// 模型限制 - 始终提交这些字段
|
||||
data.enableModelRestriction = form.enableModelRestriction
|
||||
data.restrictedModels = form.restrictedModels
|
||||
|
||||
|
||||
// 客户端限制 - 始终提交这些字段
|
||||
data.enableClientRestriction = form.enableClientRestriction
|
||||
data.allowedClients = form.allowedClients
|
||||
|
||||
|
||||
const result = await apiClient.put(`/admin/api-keys/${props.apiKey.id}`, data)
|
||||
|
||||
|
||||
if (result.success) {
|
||||
emit('success')
|
||||
emit('close')
|
||||
@@ -683,12 +653,12 @@ const refreshAccounts = async () => {
|
||||
apiClient.get('/admin/gemini-accounts'),
|
||||
apiClient.get('/admin/account-groups')
|
||||
])
|
||||
|
||||
|
||||
// 合并Claude OAuth账户和Claude Console账户
|
||||
const claudeAccounts = []
|
||||
|
||||
|
||||
if (claudeData.success) {
|
||||
claudeData.data?.forEach(account => {
|
||||
claudeData.data?.forEach((account) => {
|
||||
claudeAccounts.push({
|
||||
...account,
|
||||
platform: 'claude-oauth',
|
||||
@@ -696,9 +666,9 @@ const refreshAccounts = async () => {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
if (claudeConsoleData.success) {
|
||||
claudeConsoleData.data?.forEach(account => {
|
||||
claudeConsoleData.data?.forEach((account) => {
|
||||
claudeAccounts.push({
|
||||
...account,
|
||||
platform: 'claude-console',
|
||||
@@ -706,23 +676,23 @@ const refreshAccounts = async () => {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
localAccounts.value.claude = claudeAccounts
|
||||
|
||||
|
||||
if (geminiData.success) {
|
||||
localAccounts.value.gemini = (geminiData.data || []).map(account => ({
|
||||
localAccounts.value.gemini = (geminiData.data || []).map((account) => ({
|
||||
...account,
|
||||
isDedicated: account.accountType === 'dedicated'
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
// 处理分组数据
|
||||
if (groupsData.success) {
|
||||
const allGroups = groupsData.data || []
|
||||
localAccounts.value.claudeGroups = allGroups.filter(g => g.platform === 'claude')
|
||||
localAccounts.value.geminiGroups = allGroups.filter(g => g.platform === 'gemini')
|
||||
localAccounts.value.claudeGroups = allGroups.filter((g) => g.platform === 'claude')
|
||||
localAccounts.value.geminiGroups = allGroups.filter((g) => g.platform === 'gemini')
|
||||
}
|
||||
|
||||
|
||||
showToast('账号列表已刷新', 'success')
|
||||
} catch (error) {
|
||||
showToast('刷新账号列表失败', 'error')
|
||||
@@ -736,7 +706,7 @@ onMounted(async () => {
|
||||
// 加载支持的客户端和已存在的标签
|
||||
supportedClients.value = await clientsStore.loadSupportedClients()
|
||||
availableTags.value = await apiKeysStore.fetchTags()
|
||||
|
||||
|
||||
// 初始化账号数据
|
||||
if (props.accounts) {
|
||||
localAccounts.value = {
|
||||
@@ -746,7 +716,7 @@ onMounted(async () => {
|
||||
geminiGroups: props.accounts.geminiGroups || []
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
form.name = props.apiKey.name
|
||||
form.tokenLimit = props.apiKey.tokenLimit || ''
|
||||
form.rateLimitWindow = props.apiKey.rateLimitWindow || ''
|
||||
@@ -772,4 +742,4 @@ onMounted(async () => {
|
||||
|
||||
<style scoped>
|
||||
/* 表单样式由全局样式提供 */
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,42 +1,39 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="show"
|
||||
class="fixed inset-0 modal z-50 flex items-center justify-center p-4"
|
||||
>
|
||||
<div v-if="show" class="modal fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
<!-- 模态框内容 -->
|
||||
<div class="modal-content w-full max-w-lg p-8 mx-auto">
|
||||
<div class="modal-content mx-auto w-full max-w-lg p-8">
|
||||
<!-- 头部 -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 bg-gradient-to-br from-amber-500 to-orange-600 rounded-xl flex items-center justify-center">
|
||||
<div
|
||||
class="flex h-10 w-10 items-center justify-center rounded-xl bg-gradient-to-br from-amber-500 to-orange-600"
|
||||
>
|
||||
<i class="fas fa-clock text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-xl font-bold text-gray-900">
|
||||
修改过期时间
|
||||
</h3>
|
||||
<h3 class="text-xl font-bold text-gray-900">修改过期时间</h3>
|
||||
<p class="text-sm text-gray-600">
|
||||
为 "{{ apiKey.name || 'API Key' }}" 设置新的过期时间
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="text-gray-400 hover:text-gray-600 transition-colors"
|
||||
class="text-gray-400 transition-colors hover:text-gray-600"
|
||||
@click="$emit('close')"
|
||||
>
|
||||
<i class="fas fa-times text-xl" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="space-y-6">
|
||||
<!-- 当前状态显示 -->
|
||||
<div class="bg-gradient-to-r from-gray-50 to-gray-100 rounded-lg p-4 border border-gray-200">
|
||||
<div
|
||||
class="rounded-lg border border-gray-200 bg-gradient-to-r from-gray-50 to-gray-100 p-4"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-xs text-gray-600 font-medium mb-1">
|
||||
当前过期时间
|
||||
</p>
|
||||
<p class="mb-1 text-xs font-medium text-gray-600">当前过期时间</p>
|
||||
<p class="text-sm font-semibold text-gray-800">
|
||||
<template v-if="apiKey.expiresAt">
|
||||
{{ formatExpireDate(apiKey.expiresAt) }}
|
||||
@@ -54,26 +51,28 @@
|
||||
</template>
|
||||
</p>
|
||||
</div>
|
||||
<div class="w-12 h-12 bg-white rounded-lg flex items-center justify-center shadow-sm">
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-white shadow-sm">
|
||||
<i
|
||||
:class="[
|
||||
'fas fa-hourglass-half text-lg',
|
||||
apiKey.expiresAt && isExpired(apiKey.expiresAt) ? 'text-red-500' : 'text-gray-400'
|
||||
apiKey.expiresAt && isExpired(apiKey.expiresAt)
|
||||
? 'text-red-500'
|
||||
: 'text-gray-400'
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 快捷选项 -->
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-3">选择新的期限</label>
|
||||
<div class="grid grid-cols-3 gap-2 mb-3">
|
||||
<label class="mb-3 block text-sm font-semibold text-gray-700">选择新的期限</label>
|
||||
<div class="mb-3 grid grid-cols-3 gap-2">
|
||||
<button
|
||||
v-for="option in quickOptions"
|
||||
:key="option.value"
|
||||
:class="[
|
||||
'px-3 py-2 rounded-lg text-sm font-medium transition-all',
|
||||
'rounded-lg px-3 py-2 text-sm font-medium transition-all',
|
||||
localForm.expireDuration === option.value
|
||||
? 'bg-blue-500 text-white shadow-md'
|
||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
||||
@@ -84,7 +83,7 @@
|
||||
</button>
|
||||
<button
|
||||
:class="[
|
||||
'px-3 py-2 rounded-lg text-sm font-medium transition-all',
|
||||
'rounded-lg px-3 py-2 text-sm font-medium transition-all',
|
||||
localForm.expireDuration === 'custom'
|
||||
? 'bg-blue-500 text-white shadow-md'
|
||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
||||
@@ -96,33 +95,28 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 自定义日期选择 -->
|
||||
<div
|
||||
v-if="localForm.expireDuration === 'custom'"
|
||||
class="animate-fadeIn"
|
||||
>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">选择日期和时间</label>
|
||||
<input
|
||||
v-model="localForm.customExpireDate"
|
||||
type="datetime-local"
|
||||
<div v-if="localForm.expireDuration === 'custom'" class="animate-fadeIn">
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700">选择日期和时间</label>
|
||||
<input
|
||||
v-model="localForm.customExpireDate"
|
||||
class="form-input w-full"
|
||||
:min="minDateTime"
|
||||
type="datetime-local"
|
||||
@change="updateCustomExpiryPreview"
|
||||
>
|
||||
<p class="text-xs text-gray-500 mt-2">
|
||||
选择一个未来的日期和时间作为过期时间
|
||||
</p>
|
||||
/>
|
||||
<p class="mt-2 text-xs text-gray-500">选择一个未来的日期和时间作为过期时间</p>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 预览新的过期时间 -->
|
||||
<div
|
||||
<div
|
||||
v-if="localForm.expiresAt !== apiKey.expiresAt"
|
||||
class="bg-gradient-to-r from-blue-50 to-indigo-50 rounded-lg p-4 border border-blue-200"
|
||||
class="rounded-lg border border-blue-200 bg-gradient-to-r from-blue-50 to-indigo-50 p-4"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-xs text-blue-700 font-medium mb-1">
|
||||
<p class="mb-1 text-xs font-medium text-blue-700">
|
||||
<i class="fas fa-arrow-right mr-1" />
|
||||
新的过期时间
|
||||
</p>
|
||||
@@ -143,33 +137,27 @@
|
||||
</template>
|
||||
</p>
|
||||
</div>
|
||||
<div class="w-12 h-12 bg-white rounded-lg flex items-center justify-center shadow-sm">
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-white shadow-sm">
|
||||
<i class="fas fa-check text-lg text-green-500" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="flex gap-3 pt-2">
|
||||
<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"
|
||||
class="flex-1 rounded-lg bg-gray-100 px-4 py-2.5 font-semibold text-gray-700 transition-colors hover:bg-gray-200"
|
||||
@click="$emit('close')"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
class="flex-1 btn btn-primary py-2.5 px-4 font-semibold"
|
||||
class="btn btn-primary flex-1 px-4 py-2.5 font-semibold"
|
||||
:disabled="saving || localForm.expiresAt === apiKey.expiresAt"
|
||||
@click="handleSave"
|
||||
>
|
||||
<div
|
||||
v-if="saving"
|
||||
class="loading-spinner mr-2"
|
||||
/>
|
||||
<i
|
||||
v-else
|
||||
class="fas fa-save mr-2"
|
||||
/>
|
||||
<div v-if="saving" class="loading-spinner mr-2" />
|
||||
<i v-else class="fas fa-save mr-2" />
|
||||
{{ saving ? '保存中...' : '保存更改' }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -224,23 +212,29 @@ const minDateTime = computed(() => {
|
||||
})
|
||||
|
||||
// 监听显示状态,初始化表单
|
||||
watch(() => props.show, (newVal) => {
|
||||
if (newVal) {
|
||||
initializeForm()
|
||||
watch(
|
||||
() => props.show,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
initializeForm()
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
// 监听 apiKey 变化,重新初始化
|
||||
watch(() => props.apiKey?.id, (newId) => {
|
||||
if (newId && props.show) {
|
||||
initializeForm()
|
||||
watch(
|
||||
() => props.apiKey?.id,
|
||||
(newId) => {
|
||||
if (newId && props.show) {
|
||||
initializeForm()
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
// 初始化表单
|
||||
const initializeForm = () => {
|
||||
saving.value = false
|
||||
|
||||
|
||||
if (props.apiKey.expiresAt) {
|
||||
localForm.expireDuration = 'custom'
|
||||
localForm.customExpireDate = new Date(props.apiKey.expiresAt).toISOString().slice(0, 16)
|
||||
@@ -255,23 +249,23 @@ const initializeForm = () => {
|
||||
// 选择快捷选项
|
||||
const selectQuickOption = (value) => {
|
||||
localForm.expireDuration = value
|
||||
|
||||
|
||||
if (!value) {
|
||||
localForm.expiresAt = null
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if (value === 'custom') {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
const now = new Date()
|
||||
const match = value.match(/(\d+)([dhmy])/)
|
||||
|
||||
|
||||
if (match) {
|
||||
const [, num, unit] = match
|
||||
const amount = parseInt(num)
|
||||
|
||||
|
||||
switch (unit) {
|
||||
case 'd':
|
||||
now.setDate(now.getDate() + amount)
|
||||
@@ -286,7 +280,7 @@ const selectQuickOption = (value) => {
|
||||
now.setFullYear(now.getFullYear() + amount)
|
||||
break
|
||||
}
|
||||
|
||||
|
||||
localForm.expiresAt = now.toISOString()
|
||||
}
|
||||
}
|
||||
@@ -320,12 +314,12 @@ const isExpired = (dateString) => {
|
||||
// 获取过期状态
|
||||
const getExpiryStatus = (expiresAt) => {
|
||||
if (!expiresAt) return null
|
||||
|
||||
|
||||
const now = new Date()
|
||||
const expiryDate = new Date(expiresAt)
|
||||
const diffMs = expiryDate - now
|
||||
const diffDays = Math.ceil(diffMs / (1000 * 60 * 60 * 24))
|
||||
|
||||
|
||||
if (diffMs < 0) {
|
||||
return {
|
||||
text: '已过期',
|
||||
@@ -396,7 +390,11 @@ defineExpose({
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,97 +1,100 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div class="fixed inset-0 modal z-50 flex items-center justify-center p-4">
|
||||
<div class="modal-content w-full max-w-lg p-8 mx-auto max-h-[90vh] overflow-y-auto custom-scrollbar">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div class="modal fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
<div
|
||||
class="modal-content custom-scrollbar mx-auto max-h-[90vh] w-full max-w-lg overflow-y-auto p-8"
|
||||
>
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-12 h-12 bg-gradient-to-br from-green-500 to-green-600 rounded-xl flex items-center justify-center">
|
||||
<i class="fas fa-check text-white text-lg" />
|
||||
<div
|
||||
class="flex h-12 w-12 items-center justify-center rounded-xl bg-gradient-to-br from-green-500 to-green-600"
|
||||
>
|
||||
<i class="fas fa-check text-lg text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-xl font-bold text-gray-900">
|
||||
API Key 创建成功
|
||||
</h3>
|
||||
<p class="text-sm text-gray-600">
|
||||
请妥善保存您的 API Key
|
||||
</p>
|
||||
<h3 class="text-xl font-bold text-gray-900">API Key 创建成功</h3>
|
||||
<p class="text-sm text-gray-600">请妥善保存您的 API Key</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="text-gray-400 hover:text-gray-600 transition-colors"
|
||||
<button
|
||||
class="text-gray-400 transition-colors hover:text-gray-600"
|
||||
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="mb-6 border-l-4 border-amber-400 bg-amber-50 p-4">
|
||||
<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
|
||||
class="mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-lg bg-amber-400"
|
||||
>
|
||||
<i class="fas fa-exclamation-triangle text-sm text-white" />
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h5 class="font-semibold text-amber-900 mb-1">
|
||||
重要提醒
|
||||
</h5>
|
||||
<h5 class="mb-1 font-semibold text-amber-900">重要提醒</h5>
|
||||
<p class="text-sm text-amber-800">
|
||||
这是您唯一能看到完整 API Key 的机会。关闭此窗口后,系统将不再显示完整的 API Key。请立即复制并妥善保存。
|
||||
这是您唯一能看到完整 API Key 的机会。关闭此窗口后,系统将不再显示完整的 API
|
||||
Key。请立即复制并妥善保存。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- API Key 信息 -->
|
||||
<div class="space-y-4 mb-6">
|
||||
<div class="mb-6 space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">API Key 名称</label>
|
||||
<div class="p-3 bg-gray-50 rounded-lg border">
|
||||
<span class="text-gray-900 font-medium">{{ apiKey.name }}</span>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700">API Key 名称</label>
|
||||
<div class="rounded-lg border bg-gray-50 p-3">
|
||||
<span class="font-medium text-gray-900">{{ apiKey.name }}</span>
|
||||
</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">
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700">备注</label>
|
||||
<div class="rounded-lg border bg-gray-50 p-3">
|
||||
<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>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700">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">
|
||||
<div
|
||||
class="flex min-h-[60px] items-center break-all rounded-lg border bg-gray-900 p-4 pr-14 font-mono text-sm text-white"
|
||||
>
|
||||
{{ getDisplayedApiKey() }}
|
||||
</div>
|
||||
<div class="absolute top-3 right-3">
|
||||
<button
|
||||
type="button"
|
||||
class="btn-icon-sm hover:bg-gray-800 bg-gray-700"
|
||||
<div class="absolute right-3 top-3">
|
||||
<button
|
||||
class="btn-icon-sm bg-gray-700 hover:bg-gray-800"
|
||||
:title="showFullKey ? '隐藏API Key' : '显示完整API Key'"
|
||||
type="button"
|
||||
@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">
|
||||
<p class="mt-2 text-xs text-gray-500">
|
||||
点击眼睛图标切换显示模式,使用下方按钮复制完整 API Key
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="flex gap-3">
|
||||
<button
|
||||
class="flex-1 btn btn-primary py-3 px-6 font-semibold flex items-center justify-center gap-2"
|
||||
<button
|
||||
class="btn btn-primary flex flex-1 items-center justify-center gap-2 px-6 py-3 font-semibold"
|
||||
@click="copyApiKey"
|
||||
>
|
||||
<i class="fas fa-copy" />
|
||||
复制 API Key
|
||||
</button>
|
||||
<button
|
||||
class="px-6 py-3 bg-gray-200 text-gray-800 rounded-xl font-semibold hover:bg-gray-300 transition-colors border border-gray-300"
|
||||
<button
|
||||
class="rounded-xl border border-gray-300 bg-gray-200 px-6 py-3 font-semibold text-gray-800 transition-colors hover:bg-gray-300"
|
||||
@click="handleClose"
|
||||
>
|
||||
我已保存
|
||||
@@ -126,13 +129,15 @@ const toggleKeyVisibility = () => {
|
||||
const getDisplayedApiKey = () => {
|
||||
const key = props.apiKey.apiKey || props.apiKey.key || ''
|
||||
if (!key) return ''
|
||||
|
||||
|
||||
if (showFullKey.value) {
|
||||
return key
|
||||
} else {
|
||||
// 显示前8个字符和后4个字符,中间用●代替
|
||||
if (key.length <= 12) return key
|
||||
return key.substring(0, 8) + '●'.repeat(Math.max(0, key.length - 12)) + key.substring(key.length - 4)
|
||||
return (
|
||||
key.substring(0, 8) + '●'.repeat(Math.max(0, key.length - 12)) + key.substring(key.length - 4)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,7 +148,7 @@ const copyApiKey = async () => {
|
||||
showToast('API Key 不存在', 'error')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(key)
|
||||
showToast('API Key 已复制到剪贴板', 'success')
|
||||
@@ -202,9 +207,7 @@ const handleDirectClose = async () => {
|
||||
}
|
||||
} else {
|
||||
// 降级方案
|
||||
const confirmed = confirm(
|
||||
'您还没有保存API Key,关闭后将无法再次查看。\n\n确定要关闭吗?'
|
||||
)
|
||||
const confirmed = confirm('您还没有保存API Key,关闭后将无法再次查看。\n\n确定要关闭吗?')
|
||||
if (confirmed) {
|
||||
emit('close')
|
||||
}
|
||||
@@ -217,4 +220,4 @@ pre {
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,116 +1,92 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div class="fixed inset-0 modal z-50 flex items-center justify-center p-4">
|
||||
<div class="modal-content w-full max-w-md p-8 mx-auto max-h-[90vh] flex flex-col">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div class="modal fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
<div class="modal-content mx-auto flex max-h-[90vh] w-full max-w-md flex-col p-8">
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
<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="flex h-10 w-10 items-center justify-center rounded-xl bg-gradient-to-br from-green-500 to-green-600"
|
||||
>
|
||||
<i class="fas fa-clock text-white" />
|
||||
</div>
|
||||
<h3 class="text-xl font-bold text-gray-900">
|
||||
续期 API Key
|
||||
</h3>
|
||||
<h3 class="text-xl font-bold text-gray-900">续期 API Key</h3>
|
||||
</div>
|
||||
<button
|
||||
class="text-gray-400 hover:text-gray-600 transition-colors"
|
||||
<button
|
||||
class="text-gray-400 transition-colors hover:text-gray-600"
|
||||
@click="$emit('close')"
|
||||
>
|
||||
<i class="fas fa-times text-xl" />
|
||||
</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="modal-scroll-content custom-scrollbar flex-1 space-y-6">
|
||||
<div class="rounded-lg border border-blue-200 bg-blue-50 p-4">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="w-8 h-8 bg-blue-500 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
<i class="fas fa-info text-white text-sm" />
|
||||
<div
|
||||
class="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg bg-blue-500"
|
||||
>
|
||||
<i class="fas fa-info text-sm text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-semibold text-gray-800 mb-1">
|
||||
API Key 信息
|
||||
</h4>
|
||||
<h4 class="mb-1 font-semibold text-gray-800">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 class="mt-1 text-xs text-gray-600">
|
||||
当前过期时间:{{
|
||||
apiKey.expiresAt ? formatExpireDate(apiKey.expiresAt) : '永不过期'
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-3">续期时长</label>
|
||||
<select
|
||||
v-model="form.renewDuration"
|
||||
<label class="mb-3 block text-sm font-semibold text-gray-700">续期时长</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>
|
||||
<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"
|
||||
<div v-if="form.renewDuration === 'custom'" class="mt-3">
|
||||
<input
|
||||
v-model="form.customExpireDate"
|
||||
class="form-input w-full"
|
||||
:min="minDateTime"
|
||||
type="datetime-local"
|
||||
@change="updateCustomRenewExpireAt"
|
||||
>
|
||||
/>
|
||||
</div>
|
||||
<p
|
||||
v-if="form.newExpiresAt"
|
||||
class="text-xs text-gray-500 mt-2"
|
||||
>
|
||||
<p v-if="form.newExpiresAt" class="mt-2 text-xs text-gray-500">
|
||||
新的过期时间:{{ 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"
|
||||
<button
|
||||
class="flex-1 rounded-xl bg-gray-100 px-6 py-3 font-semibold text-gray-700 transition-colors hover:bg-gray-200"
|
||||
type="button"
|
||||
@click="$emit('close')"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
<button
|
||||
class="btn btn-primary flex-1 px-6 py-3 font-semibold"
|
||||
:disabled="loading || !form.renewDuration"
|
||||
class="btn btn-primary flex-1 py-3 px-6 font-semibold"
|
||||
type="button"
|
||||
@click="renewApiKey"
|
||||
>
|
||||
<div
|
||||
v-if="loading"
|
||||
class="loading-spinner mr-2"
|
||||
/>
|
||||
<i
|
||||
v-else
|
||||
class="fas fa-clock mr-2"
|
||||
/>
|
||||
<div v-if="loading" class="loading-spinner mr-2" />
|
||||
<i v-else class="fas fa-clock mr-2" />
|
||||
{{ loading ? '续期中...' : '确认续期' }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -122,7 +98,6 @@
|
||||
<script setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { showToast } from '@/utils/toast'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { apiClient } from '@/config/api'
|
||||
|
||||
const props = defineProps({
|
||||
@@ -134,7 +109,6 @@ const props = defineProps({
|
||||
|
||||
const emit = defineEmits(['close', 'success'])
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const loading = ref(false)
|
||||
|
||||
// 表单数据
|
||||
@@ -174,28 +148,29 @@ const updateRenewExpireAt = () => {
|
||||
form.newExpiresAt = null
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if (form.renewDuration === 'permanent') {
|
||||
form.newExpiresAt = null
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if (form.renewDuration === 'custom') {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// 计算新的过期时间
|
||||
const baseDate = props.apiKey.expiresAt && new Date(props.apiKey.expiresAt) > new Date()
|
||||
? new Date(props.apiKey.expiresAt)
|
||||
: new Date()
|
||||
|
||||
const baseDate =
|
||||
props.apiKey.expiresAt && new Date(props.apiKey.expiresAt) > new Date()
|
||||
? new Date(props.apiKey.expiresAt)
|
||||
: new Date()
|
||||
|
||||
const duration = form.renewDuration
|
||||
const match = duration.match(/(\d+)([dhmy])/)
|
||||
|
||||
|
||||
if (match) {
|
||||
const [, value, unit] = match
|
||||
const num = parseInt(value)
|
||||
|
||||
|
||||
switch (unit) {
|
||||
case 'd':
|
||||
baseDate.setDate(baseDate.getDate() + num)
|
||||
@@ -210,7 +185,7 @@ const updateRenewExpireAt = () => {
|
||||
baseDate.setFullYear(baseDate.getFullYear() + num)
|
||||
break
|
||||
}
|
||||
|
||||
|
||||
form.newExpiresAt = baseDate.toISOString()
|
||||
}
|
||||
}
|
||||
@@ -225,14 +200,14 @@ const updateCustomRenewExpireAt = () => {
|
||||
// 续期 API Key
|
||||
const renewApiKey = async () => {
|
||||
loading.value = true
|
||||
|
||||
|
||||
try {
|
||||
const data = {
|
||||
expiresAt: form.renewDuration === 'permanent' ? null : form.newExpiresAt
|
||||
}
|
||||
|
||||
|
||||
const result = await apiClient.put(`/admin/api-keys/${props.apiKey.id}`, data)
|
||||
|
||||
|
||||
if (result.success) {
|
||||
showToast('API Key 续期成功', 'success')
|
||||
emit('success')
|
||||
@@ -253,4 +228,4 @@ updateRenewExpireAt()
|
||||
|
||||
<style scoped>
|
||||
/* 表单样式由全局样式提供 */
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,88 +1,87 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="show"
|
||||
class="fixed inset-0 modal z-50 flex items-center justify-center p-3 sm:p-4"
|
||||
>
|
||||
<div v-if="show" class="modal fixed inset-0 z-50 flex items-center justify-center p-3 sm:p-4">
|
||||
<!-- 背景遮罩 -->
|
||||
<div
|
||||
class="fixed inset-0 bg-gray-900 bg-opacity-50 backdrop-blur-sm"
|
||||
@click="close"
|
||||
/>
|
||||
|
||||
<div class="fixed inset-0 bg-gray-900 bg-opacity-50 backdrop-blur-sm" @click="close" />
|
||||
|
||||
<!-- 模态框 -->
|
||||
<div class="modal-content w-[95%] sm:w-full max-w-2xl sm:max-w-3xl p-4 sm:p-6 md:p-8 mx-auto max-h-[90vh] flex flex-col relative">
|
||||
<div
|
||||
class="modal-content relative mx-auto flex max-h-[90vh] w-[95%] max-w-2xl flex-col p-4 sm:w-full sm:max-w-3xl sm:p-6 md:p-8"
|
||||
>
|
||||
<!-- 标题栏 -->
|
||||
<div class="flex items-center justify-between mb-4 sm:mb-6">
|
||||
<div class="mb-4 flex items-center justify-between sm:mb-6">
|
||||
<div class="flex items-center gap-2 sm:gap-3">
|
||||
<div class="w-8 h-8 sm:w-10 sm:h-10 bg-gradient-to-br from-blue-500 to-blue-600 rounded-lg sm:rounded-xl flex items-center justify-center">
|
||||
<i class="fas fa-chart-line text-white text-sm sm:text-base" />
|
||||
<div
|
||||
class="flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-blue-500 to-blue-600 sm:h-10 sm:w-10 sm:rounded-xl"
|
||||
>
|
||||
<i class="fas fa-chart-line text-sm text-white sm:text-base" />
|
||||
</div>
|
||||
<h3 class="text-lg sm:text-xl font-bold text-gray-900">
|
||||
<h3 class="text-lg font-bold text-gray-900 sm:text-xl">
|
||||
使用统计详情 - {{ apiKey.name }}
|
||||
</h3>
|
||||
</div>
|
||||
<button
|
||||
class="text-gray-400 hover:text-gray-600 transition-colors p-1"
|
||||
@click="close"
|
||||
>
|
||||
<button class="p-1 text-gray-400 transition-colors hover:text-gray-600" @click="close">
|
||||
<i class="fas fa-times text-lg sm:text-xl" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 内容区 -->
|
||||
<div class="modal-scroll-content custom-scrollbar flex-1 overflow-y-auto">
|
||||
<!-- 总体统计卡片 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||
<div class="mb-6 grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<!-- 请求统计卡片 -->
|
||||
<div class="bg-gradient-to-br from-blue-50 to-blue-100 rounded-lg p-4 border border-blue-200">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div
|
||||
class="rounded-lg border border-blue-200 bg-gradient-to-br from-blue-50 to-blue-100 p-4"
|
||||
>
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<span class="text-sm font-medium text-gray-700">总请求数</span>
|
||||
<i class="fas fa-paper-plane text-blue-500" />
|
||||
</div>
|
||||
<div class="text-2xl font-bold text-gray-900">
|
||||
{{ formatNumber(totalRequests) }}
|
||||
</div>
|
||||
<div class="text-xs text-gray-600 mt-1">
|
||||
<div class="mt-1 text-xs text-gray-600">
|
||||
今日: {{ formatNumber(dailyRequests) }} 次
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Token统计卡片 -->
|
||||
<div class="bg-gradient-to-br from-green-50 to-green-100 rounded-lg p-4 border border-green-200">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div
|
||||
class="rounded-lg border border-green-200 bg-gradient-to-br from-green-50 to-green-100 p-4"
|
||||
>
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<span class="text-sm font-medium text-gray-700">总Token数</span>
|
||||
<i class="fas fa-coins text-green-500" />
|
||||
</div>
|
||||
<div class="text-2xl font-bold text-gray-900">
|
||||
{{ formatTokenCount(totalTokens) }}
|
||||
</div>
|
||||
<div class="text-xs text-gray-600 mt-1">
|
||||
<div class="mt-1 text-xs text-gray-600">
|
||||
今日: {{ formatTokenCount(dailyTokens) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 费用统计卡片 -->
|
||||
<div class="bg-gradient-to-br from-yellow-50 to-yellow-100 rounded-lg p-4 border border-yellow-200">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div
|
||||
class="rounded-lg border border-yellow-200 bg-gradient-to-br from-yellow-50 to-yellow-100 p-4"
|
||||
>
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<span class="text-sm font-medium text-gray-700">总费用</span>
|
||||
<i class="fas fa-dollar-sign text-yellow-600" />
|
||||
</div>
|
||||
<div class="text-2xl font-bold text-gray-900">
|
||||
${{ totalCost.toFixed(4) }}
|
||||
</div>
|
||||
<div class="text-xs text-gray-600 mt-1">
|
||||
今日: ${{ dailyCost.toFixed(4) }}
|
||||
</div>
|
||||
<div class="text-2xl font-bold text-gray-900">${{ totalCost.toFixed(4) }}</div>
|
||||
<div class="mt-1 text-xs text-gray-600">今日: ${{ dailyCost.toFixed(4) }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 平均统计卡片 -->
|
||||
<div class="bg-gradient-to-br from-purple-50 to-purple-100 rounded-lg p-4 border border-purple-200">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div
|
||||
class="rounded-lg border border-purple-200 bg-gradient-to-br from-purple-50 to-purple-100 p-4"
|
||||
>
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<span class="text-sm font-medium text-gray-700">平均速率</span>
|
||||
<i class="fas fa-tachometer-alt text-purple-500" />
|
||||
</div>
|
||||
<div class="text-sm space-y-1">
|
||||
<div class="space-y-1 text-sm">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600">RPM:</span>
|
||||
<span class="font-semibold">{{ rpm }}</span>
|
||||
@@ -97,14 +96,14 @@
|
||||
|
||||
<!-- Token详细分布 -->
|
||||
<div class="mb-6">
|
||||
<h4 class="text-sm font-semibold text-gray-700 mb-3 flex items-center">
|
||||
<i class="fas fa-chart-pie text-indigo-500 mr-2" />
|
||||
<h4 class="mb-3 flex items-center text-sm font-semibold text-gray-700">
|
||||
<i class="fas fa-chart-pie mr-2 text-indigo-500" />
|
||||
Token 使用分布
|
||||
</h4>
|
||||
<div class="bg-gray-50 rounded-lg p-4 space-y-3">
|
||||
<div class="space-y-3 rounded-lg bg-gray-50 p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-arrow-down text-green-500 mr-2" />
|
||||
<i class="fas fa-arrow-down mr-2 text-green-500" />
|
||||
<span class="text-sm text-gray-600">输入 Token</span>
|
||||
</div>
|
||||
<span class="text-sm font-semibold text-gray-900">
|
||||
@@ -113,31 +112,25 @@
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-arrow-up text-blue-500 mr-2" />
|
||||
<i class="fas fa-arrow-up mr-2 text-blue-500" />
|
||||
<span class="text-sm text-gray-600">输出 Token</span>
|
||||
</div>
|
||||
<span class="text-sm font-semibold text-gray-900">
|
||||
{{ formatTokenCount(outputTokens) }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="cacheCreateTokens > 0"
|
||||
class="flex items-center justify-between"
|
||||
>
|
||||
<div v-if="cacheCreateTokens > 0" class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-save text-purple-500 mr-2" />
|
||||
<i class="fas fa-save mr-2 text-purple-500" />
|
||||
<span class="text-sm text-gray-600">缓存创建 Token</span>
|
||||
</div>
|
||||
<span class="text-sm font-semibold text-purple-600">
|
||||
{{ formatTokenCount(cacheCreateTokens) }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="cacheReadTokens > 0"
|
||||
class="flex items-center justify-between"
|
||||
>
|
||||
<div v-if="cacheReadTokens > 0" class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-download text-purple-500 mr-2" />
|
||||
<i class="fas fa-download mr-2 text-purple-500" />
|
||||
<span class="text-sm text-gray-600">缓存读取 Token</span>
|
||||
</div>
|
||||
<span class="text-sm font-semibold text-purple-600">
|
||||
@@ -148,33 +141,33 @@
|
||||
</div>
|
||||
|
||||
<!-- 限制信息 -->
|
||||
<div
|
||||
v-if="hasLimits"
|
||||
class="mb-6"
|
||||
>
|
||||
<h4 class="text-sm font-semibold text-gray-700 mb-3 flex items-center">
|
||||
<i class="fas fa-shield-alt text-red-500 mr-2" />
|
||||
<div v-if="hasLimits" class="mb-6">
|
||||
<h4 class="mb-3 flex items-center text-sm font-semibold text-gray-700">
|
||||
<i class="fas fa-shield-alt mr-2 text-red-500" />
|
||||
限制设置
|
||||
</h4>
|
||||
<div class="bg-gray-50 rounded-lg p-4 space-y-3">
|
||||
<div
|
||||
v-if="apiKey.dailyCostLimit > 0"
|
||||
class="space-y-2"
|
||||
>
|
||||
<div class="space-y-3 rounded-lg bg-gray-50 p-4">
|
||||
<div v-if="apiKey.dailyCostLimit > 0" class="space-y-2">
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-gray-600">每日费用限制</span>
|
||||
<span class="font-semibold text-gray-900">
|
||||
${{ apiKey.dailyCostLimit.toFixed(2) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
<div class="h-2 w-full rounded-full bg-gray-200">
|
||||
<div
|
||||
class="h-2 rounded-full transition-all duration-300"
|
||||
:class="dailyCostPercentage >= 100 ? 'bg-red-500' : dailyCostPercentage >= 80 ? 'bg-yellow-500' : 'bg-green-500'"
|
||||
:class="
|
||||
dailyCostPercentage >= 100
|
||||
? 'bg-red-500'
|
||||
: dailyCostPercentage >= 80
|
||||
? 'bg-yellow-500'
|
||||
: 'bg-green-500'
|
||||
"
|
||||
:style="{ width: Math.min(dailyCostPercentage, 100) + '%' }"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 text-right">
|
||||
<div class="text-right text-xs text-gray-500">
|
||||
已使用 {{ dailyCostPercentage.toFixed(1) }}%
|
||||
</div>
|
||||
</div>
|
||||
@@ -189,50 +182,42 @@
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="apiKey.rateLimitWindow > 0"
|
||||
class="space-y-2"
|
||||
>
|
||||
<div v-if="apiKey.rateLimitWindow > 0" class="space-y-2">
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-gray-600">时间窗口</span>
|
||||
<span class="font-semibold text-indigo-600">
|
||||
{{ apiKey.rateLimitWindow }} 分钟
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 请求次数限制 -->
|
||||
<div
|
||||
v-if="apiKey.rateLimitRequests > 0"
|
||||
class="space-y-1"
|
||||
>
|
||||
<div v-if="apiKey.rateLimitRequests > 0" class="space-y-1">
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-gray-600">请求限制</span>
|
||||
<span class="font-semibold text-gray-900">
|
||||
{{ apiKey.currentWindowRequests || 0 }} / {{ apiKey.rateLimitRequests }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
<div class="h-2 w-full rounded-full bg-gray-200">
|
||||
<div
|
||||
class="h-2 rounded-full transition-all duration-300"
|
||||
:class="windowRequestProgressColor"
|
||||
:style="{ width: windowRequestProgress + '%' }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Token使用量限制 -->
|
||||
<div
|
||||
v-if="apiKey.tokenLimit > 0"
|
||||
class="space-y-1"
|
||||
>
|
||||
<div v-if="apiKey.tokenLimit > 0" class="space-y-1">
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-gray-600">Token限制</span>
|
||||
<span class="font-semibold text-gray-900">
|
||||
{{ formatTokenCount(apiKey.currentWindowTokens || 0) }} / {{ formatTokenCount(apiKey.tokenLimit) }}
|
||||
{{ formatTokenCount(apiKey.currentWindowTokens || 0) }} /
|
||||
{{ formatTokenCount(apiKey.tokenLimit) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
<div class="h-2 w-full rounded-full bg-gray-200">
|
||||
<div
|
||||
class="h-2 rounded-full transition-all duration-300"
|
||||
:class="windowTokenProgressColor"
|
||||
:style="{ width: windowTokenProgress + '%' }"
|
||||
@@ -245,12 +230,8 @@
|
||||
</div>
|
||||
|
||||
<!-- 底部按钮 -->
|
||||
<div class="mt-4 sm:mt-6 flex justify-end gap-2 sm:gap-3">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary px-4 py-2 text-sm"
|
||||
@click="close"
|
||||
>
|
||||
<div class="mt-4 flex justify-end gap-2 sm:mt-6 sm:gap-3">
|
||||
<button class="btn btn-secondary px-4 py-2 text-sm" type="button" @click="close">
|
||||
关闭
|
||||
</button>
|
||||
</div>
|
||||
@@ -276,24 +257,26 @@ const props = defineProps({
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
// 计算属性
|
||||
const totalRequests = computed(() => (props.apiKey.usage?.total?.requests) || 0)
|
||||
const dailyRequests = computed(() => (props.apiKey.usage?.daily?.requests) || 0)
|
||||
const totalTokens = computed(() => (props.apiKey.usage?.total?.tokens) || 0)
|
||||
const dailyTokens = computed(() => (props.apiKey.usage?.daily?.tokens) || 0)
|
||||
const totalCost = computed(() => (props.apiKey.usage?.total?.cost) || 0)
|
||||
const totalRequests = computed(() => props.apiKey.usage?.total?.requests || 0)
|
||||
const dailyRequests = computed(() => props.apiKey.usage?.daily?.requests || 0)
|
||||
const totalTokens = computed(() => props.apiKey.usage?.total?.tokens || 0)
|
||||
const dailyTokens = computed(() => props.apiKey.usage?.daily?.tokens || 0)
|
||||
const totalCost = computed(() => props.apiKey.usage?.total?.cost || 0)
|
||||
const dailyCost = computed(() => props.apiKey.dailyCost || 0)
|
||||
const inputTokens = computed(() => (props.apiKey.usage?.total?.inputTokens) || 0)
|
||||
const outputTokens = computed(() => (props.apiKey.usage?.total?.outputTokens) || 0)
|
||||
const cacheCreateTokens = computed(() => (props.apiKey.usage?.total?.cacheCreateTokens) || 0)
|
||||
const cacheReadTokens = computed(() => (props.apiKey.usage?.total?.cacheReadTokens) || 0)
|
||||
const rpm = computed(() => (props.apiKey.usage?.averages?.rpm) || 0)
|
||||
const tpm = computed(() => (props.apiKey.usage?.averages?.tpm) || 0)
|
||||
const inputTokens = computed(() => props.apiKey.usage?.total?.inputTokens || 0)
|
||||
const outputTokens = computed(() => props.apiKey.usage?.total?.outputTokens || 0)
|
||||
const cacheCreateTokens = computed(() => props.apiKey.usage?.total?.cacheCreateTokens || 0)
|
||||
const cacheReadTokens = computed(() => props.apiKey.usage?.total?.cacheReadTokens || 0)
|
||||
const rpm = computed(() => props.apiKey.usage?.averages?.rpm || 0)
|
||||
const tpm = computed(() => props.apiKey.usage?.averages?.tpm || 0)
|
||||
|
||||
const hasLimits = computed(() => {
|
||||
return props.apiKey.dailyCostLimit > 0 ||
|
||||
props.apiKey.concurrencyLimit > 0 ||
|
||||
props.apiKey.rateLimitWindow > 0 ||
|
||||
props.apiKey.tokenLimit > 0
|
||||
return (
|
||||
props.apiKey.dailyCostLimit > 0 ||
|
||||
props.apiKey.concurrencyLimit > 0 ||
|
||||
props.apiKey.rateLimitWindow > 0 ||
|
||||
props.apiKey.tokenLimit > 0
|
||||
)
|
||||
})
|
||||
|
||||
const dailyCostPercentage = computed(() => {
|
||||
@@ -304,7 +287,8 @@ const dailyCostPercentage = computed(() => {
|
||||
// 窗口请求进度
|
||||
const windowRequestProgress = computed(() => {
|
||||
if (!props.apiKey.rateLimitRequests || props.apiKey.rateLimitRequests === 0) return 0
|
||||
const percentage = ((props.apiKey.currentWindowRequests || 0) / props.apiKey.rateLimitRequests) * 100
|
||||
const percentage =
|
||||
((props.apiKey.currentWindowRequests || 0) / props.apiKey.rateLimitRequests) * 100
|
||||
return Math.min(percentage, 100)
|
||||
})
|
||||
|
||||
@@ -352,4 +336,4 @@ const close = () => {
|
||||
|
||||
<style scoped>
|
||||
/* 使用项目的通用样式,不需要额外定义 */
|
||||
</style>
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user