|
|
|
<div
|
|
x-cloak
|
|
x-show="selectedKeys.length > 0"
|
|
class="fixed bottom-4 left-1/2 transform -translate-x-1/2 bg-gradient-to-r from-blue-50 to-purple-50 rounded-lg shadow-xl px-4 py-3 flex items-center space-x-4 z-30 bulk-toolbar"
|
|
style="backdrop-filter: blur(8px); border: 1px solid rgba(99, 102, 241, 0.2);"
|
|
x-transition:enter="transition ease-out duration-300"
|
|
x-transition:enter-start="opacity-0 transform translate-y-4"
|
|
x-transition:enter-end="opacity-100 transform translate-y-0"
|
|
x-transition:leave="transition ease-in duration-200"
|
|
x-transition:leave-start="opacity-100 transform translate-y-0"
|
|
x-transition:leave-end="opacity-0 transform translate-y-4"
|
|
>
|
|
|
|
<div class="flex items-center space-x-3">
|
|
<div class="flex items-center justify-center h-8 min-w-8 px-2 rounded-full bg-gradient-to-r from-blue-400 to-indigo-500 text-white font-semibold text-sm shadow-sm">
|
|
<span x-text="selectedKeys.length"></span>
|
|
</div>
|
|
|
|
<div class="flex flex-col">
|
|
<span class="text-xs text-gray-500 uppercase tracking-wide">已选项目</span>
|
|
|
|
<button
|
|
@click="toggleSelectAll"
|
|
class="text-xs text-indigo-600 hover:text-indigo-800 transition-colors font-medium"
|
|
x-text="isAllSelected ? '取消全选' : '全选'"
|
|
></button>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<div class="h-10 w-px bg-gradient-to-b from-transparent via-indigo-200 to-transparent"></div>
|
|
|
|
|
|
<div class="flex space-x-2 justify-end">
|
|
|
|
<button
|
|
@click="bulkCopyApiKeys()"
|
|
class="group relative inline-flex items-center px-4 py-2 overflow-hidden border border-transparent rounded-lg shadow-md text-sm font-medium text-white bg-gradient-to-r from-blue-500 to-indigo-500 hover:from-blue-600 hover:to-indigo-600 focus:outline-none transition-all duration-300 transform hover:scale-105"
|
|
>
|
|
|
|
<span class="absolute inset-0 w-full h-full bg-gradient-to-r from-indigo-600 to-blue-600 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></span>
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="relative z-10 h-4 w-4 mr-2 group-hover:animate-pulse" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3" />
|
|
</svg>
|
|
<span class="relative z-10">批量复制</span>
|
|
</button>
|
|
|
|
|
|
<button
|
|
@click="bulkDeleteApiKeys()"
|
|
class="group relative inline-flex items-center px-4 py-2 overflow-hidden border border-transparent rounded-lg shadow-md text-sm font-medium text-white bg-gradient-to-r from-red-500 to-pink-500 hover:from-red-600 hover:to-pink-600 focus:outline-none transition-all duration-300 transform hover:scale-105"
|
|
>
|
|
|
|
<span class="absolute inset-0 w-full h-full bg-gradient-to-r from-pink-600 to-red-600 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></span>
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="relative z-10 h-4 w-4 mr-2 group-hover:animate-pulse" viewBox="0 0 20 20" fill="currentColor">
|
|
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
|
|
</svg>
|
|
<span class="relative z-10">批量删除</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<div class="space-y-6">
|
|
|
|
{% for platform in platforms %}
|
|
{% set platform_keys = grouped_keys.get(platform.id, {}).get('keys', []) %}
|
|
|
|
<div
|
|
x-cloak
|
|
x-show="!isLoading && isPlatformVisible('{{ platform.id }}') && (searchTerm === '' && hasPlatformKeys('{{ platform.id }}') || (searchTerm !== '' && apiKeys.filter(key => key.platform === '{{ platform.id }}' && (currentView === 'valid' ? key.success === true : key.success === false) && matchesSearch(key.name, key.key)).length > 0))"
|
|
x-transition:enter="transition ease-out duration-300"
|
|
x-transition:enter-start="opacity-0 transform scale-95"
|
|
x-transition:enter-end="opacity-100 transform scale-100"
|
|
class="bg-white rounded-lg shadow overflow-hidden"
|
|
>
|
|
|
|
<div
|
|
@click="togglePlatform('{{ platform.id }}')"
|
|
class="px-6 py-4 cursor-pointer flex justify-between items-center border-b border-gray-200 transition-all duration-300"
|
|
:class="{
|
|
'bg-gray-50 border-gray-200': !platformIds.includes('{{ platform.id }}')
|
|
}"
|
|
:style="{
|
|
backgroundColor: '{{ platform.id }}' in getPlatformStyles() ? getPlatformStyle('{{ platform.id }}')['background-color'] : 'rgba(243, 244, 246, 0.5)',
|
|
borderColor: '{{ platform.id }}' in getPlatformStyles() ? getPlatformStyle('{{ platform.id }}')['border-color'] : 'rgba(229, 231, 235, 1)'
|
|
}"
|
|
>
|
|
|
|
<div class="mr-3 flex items-center" @click.stop>
|
|
<div class="relative h-5 w-5">
|
|
|
|
<div
|
|
class="absolute inset-0 rounded-full border-2 transition-colors duration-200"
|
|
:class="selectedPlatforms.includes('{{ platform.id }}') ? 'border-blue-500' : 'border-gray-300'"
|
|
></div>
|
|
|
|
|
|
<div
|
|
class="absolute inset-0.5 rounded-full bg-white"
|
|
x-show="!selectedPlatforms.includes('{{ platform.id }}')"
|
|
></div>
|
|
|
|
|
|
<div
|
|
x-show="selectedPlatforms.includes('{{ platform.id }}')"
|
|
class="absolute inset-0.5 rounded-full bg-blue-200"
|
|
></div>
|
|
|
|
|
|
<input
|
|
type="checkbox"
|
|
:checked="selectedPlatforms.includes('{{ platform.id }}')"
|
|
@click="togglePlatformSelection('{{ platform.id }}')"
|
|
class="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<h2 class="text-lg font-medium flex items-center"
|
|
:style="{
|
|
color: '{{ platform.id }}' in getPlatformStyles() ? getPlatformStyle('{{ platform.id }}').color : '#111827'
|
|
}"
|
|
>
|
|
|
|
<img src="/static/img/{% if platform.id == 'anthropic' %}claude.svg{% elif platform.id == 'google' %}gemini.svg{% else %}{{ platform.id }}.svg{% endif %}" class="w-5 h-5 mr-2" alt="{{ platform.name }} 图标">
|
|
<span>{{ platform.name }}</span>
|
|
<span
|
|
x-show="getPlatformKeyCount('{{ platform.id }}') > 0"
|
|
x-text="getPlatformKeyCount('{{ platform.id }}')"
|
|
class="ml-2 platform-title-counter"
|
|
:style="{
|
|
backgroundColor: '{{ platform.id }}' in getPlatformStyles() ? getPlatformStyle('{{ platform.id }}')['background-color'] : 'rgba(224, 242, 254, 1)',
|
|
color: '{{ platform.id }}' in getPlatformStyles() ? getPlatformStyle('{{ platform.id }}').color : '#0369a1',
|
|
borderRadius: '12px',
|
|
minWidth: '1.6rem',
|
|
padding: '0 0.4rem',
|
|
width: 'auto'
|
|
}"
|
|
></span>
|
|
</h2>
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
:class="{'transform rotate-180': platformStates['{{ platform.id }}']}"
|
|
class="h-5 w-5 accordion-icon"
|
|
:style="{
|
|
color: '{{ platform.id }}' in getPlatformStyles() ? getPlatformStyle('{{ platform.id }}').color : '#6b7280'
|
|
}"
|
|
viewBox="0 0 20 20"
|
|
fill="currentColor"
|
|
>
|
|
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
|
|
</svg>
|
|
</div>
|
|
|
|
|
|
<div
|
|
x-show="platformStates['{{ platform.id }}']"
|
|
x-transition:enter="transition-all ease-out duration-400 accordion-expand"
|
|
x-transition:enter-start="opacity-0 max-h-0 overflow-hidden transform scale-y-95 origin-top"
|
|
x-transition:enter-end="opacity-100 max-h-[2000px] transform scale-y-100 origin-top"
|
|
x-transition:leave="transition-all ease-in-out duration-300 accordion-collapse"
|
|
x-transition:leave-start="opacity-100 max-h-[2000px] transform scale-y-100 origin-top"
|
|
x-transition:leave-end="opacity-0 max-h-0 overflow-hidden transform scale-y-95 origin-top"
|
|
class="divide-y divide-gray-200 accordion-content"
|
|
>
|
|
|
|
{% for key in platform_keys %}
|
|
<div
|
|
x-show="(currentView === 'valid' && {{ key.success|lower }} === true || currentView === 'invalid' && {{ key.success|lower }} === false) && matchesSearch('{{ key.name }}', '{{ key.key }}')"
|
|
class="px-6 py-4 hover:bg-gray-50 transition-colors duration-150 flex flex-col md:flex-row md:items-center md:justify-between group"
|
|
x-transition:enter="transition ease-out duration-300"
|
|
x-transition:enter-start="opacity-0"
|
|
x-transition:enter-end="opacity-100"
|
|
:class="{'bg-blue-50': selectedKeys.includes('{{ key.id }}')}"
|
|
data-key-id="{{ key.id }}"
|
|
>
|
|
|
|
<div class="mr-3 flex items-center">
|
|
<div class="relative h-5 w-5">
|
|
|
|
<div
|
|
class="absolute inset-0 rounded-full border-2 transition-colors duration-200"
|
|
:class="selectedKeys.includes('{{ key.id }}') ? 'border-blue-500' : 'border-gray-300'"
|
|
></div>
|
|
|
|
|
|
<div
|
|
class="absolute inset-0.5 rounded-full bg-white"
|
|
x-show="!selectedKeys.includes('{{ key.id }}')"
|
|
></div>
|
|
|
|
|
|
<div
|
|
x-show="selectedKeys.includes('{{ key.id }}')"
|
|
class="absolute inset-0.5 rounded-full bg-blue-200"
|
|
></div>
|
|
|
|
|
|
<input
|
|
type="checkbox"
|
|
:checked="selectedKeys.includes('{{ key.id }}')"
|
|
@click.stop="toggleKeySelection('{{ key.id }}')"
|
|
class="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex-grow min-w-0 mb-3 md:mb-0 md:mr-4">
|
|
|
|
<h3 class="text-sm font-medium text-gray-900 truncate">{{ key.name }}</h3>
|
|
|
|
|
|
<div class="mt-1.5 flex items-center">
|
|
|
|
<div class="flex items-center mr-2">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5 text-gray-500 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
|
</svg>
|
|
<span class="text-xs text-gray-600">{{ key.created_at.split('T')[0] }}</span>
|
|
</div>
|
|
|
|
|
|
<div class="flex items-center mr-2">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5 text-gray-500 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
<span class="text-xs text-gray-600">创建: {{ key.created_at.split('T')[1].split('.')[0] }}</span>
|
|
</div>
|
|
|
|
|
|
{% if key.updated_at %}
|
|
<div class="flex items-center">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5 text-gray-500 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
|
</svg>
|
|
<span class="text-xs text-gray-600">更新: {{ key.updated_at.split('T')[1].split('.')[0] }}</span>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
|
|
{% if key.success == True %}
|
|
<div class="mt-1 flex flex-wrap gap-2">
|
|
|
|
{% if key.balance %}
|
|
<div class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
余额: {{ key.balance }}
|
|
</div>
|
|
{% endif %}
|
|
|
|
|
|
{% if key.states %}
|
|
<div class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
状态: {{ key.states }}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% elif key.success == False and key.return_message %}
|
|
|
|
<div class="mt-1 flex items-center">
|
|
<div class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
{{ key.return_message }}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
|
|
<div class="mt-1 relative group key-scroll-container custom-scrollbar overflow-x-auto">
|
|
<div class="text-sm text-gray-500 font-mono py-1 pr-10">
|
|
{{ key.key }}
|
|
</div>
|
|
|
|
|
|
<div class="absolute inset-0 bg-primary-50 opacity-0 group-hover:opacity-100 transition-opacity duration-150 -z-10 rounded"></div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
<div class="flex space-x-2 justify-end">
|
|
|
|
<button
|
|
@click="copyToClipboard('{{ key.key }}', '{{ key.id }}')"
|
|
class="p-2 text-gray-500 hover:text-primary-600 rounded-md hover:bg-gray-100 transition-colors duration-150 focus:outline-none"
|
|
:class="{'text-green-600': copiedId === '{{ key.id }}'}"
|
|
data-clipboard-text="{{ key.key }}"
|
|
>
|
|
<span x-show="copiedId !== '{{ key.id }}'">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3" />
|
|
</svg>
|
|
</span>
|
|
<span x-show="copiedId === '{{ key.id }}'">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
|
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
|
|
</svg>
|
|
</span>
|
|
</button>
|
|
|
|
|
|
<button
|
|
@click="editApiKey('{{ key.id }}', '{{ key.name }}', '{{ key.key }}')"
|
|
class="p-2 text-gray-500 hover:text-blue-600 rounded-md hover:bg-gray-100 transition-colors duration-150 focus:outline-none"
|
|
>
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
|
</svg>
|
|
</button>
|
|
|
|
|
|
<button
|
|
@click="deleteApiKey('{{ key.id }}', '{{ key.name }}')"
|
|
class="p-2 text-gray-500 hover:text-red-600 rounded-md hover:bg-gray-100 transition-colors duration-150 focus:outline-none"
|
|
>
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
|
|
|
|
<div
|
|
x-show="getPlatformKeyCount('{{ platform.id }}') === 0 || (searchTerm !== '' && getPlatformKeyCount('{{ platform.id }}', true) === 0)"
|
|
class="px-6 py-8 text-center text-gray-500"
|
|
>
|
|
<div class="flex flex-col items-center">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 mb-4 text-gray-300" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
|
</svg>
|
|
<p x-show="searchTerm === '' && currentView === 'valid'">此平台暂无有效API密钥</p>
|
|
<p x-show="searchTerm === '' && currentView === 'invalid'">此平台暂无无效API密钥</p>
|
|
<p x-show="searchTerm !== ''">没有找到匹配的API密钥</p>
|
|
<button
|
|
@click="showAddModal = true; newKey.platform = '{{ platform.id }}'"
|
|
class="mt-4 inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-primary-600 hover:bg-primary-700 focus:outline-none transition-colors duration-200 space-x-2"
|
|
>
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
|
<path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd" />
|
|
</svg>
|
|
<span>添加 {{ platform.name }} 密钥</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
|