fix(admin-spa): 修复API Key创建成功后的弹窗显示问题

- 完全复刻原版的API Key成功弹窗样式
- 修复API Key字段读取问题,支持apiKey和key两种字段名
- 添加完整的显示/隐藏功能,默认隐藏API Key内容
- 实现与原版一致的警告提示和关闭确认
- 优化复制功能,添加降级方案
This commit is contained in:
shaw
2025-07-29 18:35:05 +08:00
parent 7a9e04f169
commit eff796ed8e
9 changed files with 93 additions and 90 deletions

View File

@@ -1,31 +1,36 @@
<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">
<div class="modal-content w-full max-w-lg p-8 mx-auto max-h-[90vh] overflow-y-auto custom-scrollbar">
<div class="flex items-center justify-between mb-6">
<div class="flex items-center gap-3">
<div class="w-10 h-10 bg-gradient-to-br from-green-500 to-green-600 rounded-xl flex items-center justify-center">
<i class="fas fa-check text-white"></i>
<div class="w-12 h-12 bg-gradient-to-br from-green-500 to-green-600 rounded-xl flex items-center justify-center">
<i class="fas fa-check text-white text-lg"></i>
</div>
<div>
<h3 class="text-xl font-bold text-gray-900">API Key 创建成功</h3>
<p class="text-sm text-gray-600">请妥善保存您的 API Key</p>
</div>
<h3 class="text-xl font-bold text-gray-900">API Key 创建成功</h3>
</div>
<button
@click="$emit('close')"
@click="handleClose"
class="text-gray-400 hover:text-gray-600 transition-colors"
>
<i class="fas fa-times text-xl"></i>
</button>
</div>
<!-- 成功提示 -->
<div class="bg-green-50 border border-green-200 rounded-lg p-4 mb-6">
<div class="flex items-start gap-3">
<div class="w-8 h-8 bg-green-500 rounded-lg flex items-center justify-center flex-shrink-0">
<i class="fas fa-shield-alt text-white text-sm"></i>
<!-- 警告提示 -->
<div class="bg-amber-50 border-l-4 border-amber-400 p-4 mb-6">
<div class="flex items-start">
<div class="w-6 h-6 bg-amber-400 rounded-lg flex items-center justify-center flex-shrink-0 mt-0.5">
<i class="fas fa-exclamation-triangle text-white text-sm"></i>
</div>
<div>
<h4 class="font-semibold text-gray-800 mb-1">请妥善保管您的 API Key</h4>
<p class="text-sm text-gray-600">API Key 只会显示一次关闭此窗口后将无法再次查看完整密钥请立即复制并保存到安全的地方</p>
<div class="ml-3">
<h5 class="font-semibold text-amber-900 mb-1">重要提醒</h5>
<p class="text-sm text-amber-800">
这是您唯一能看到完整 API Key 的机会关闭此窗口后系统将不再显示完整的 API Key请立即复制并妥善保存
</p>
</div>
</div>
</div>
@@ -33,68 +38,54 @@
<!-- API Key 信息 -->
<div class="space-y-4 mb-6">
<div>
<label class="block text-sm font-semibold text-gray-700 mb-2">名称</label>
<p class="text-gray-900">{{ apiKey.name }}</p>
<label class="block text-sm font-semibold text-gray-700 mb-2">API Key 名称</label>
<div class="p-3 bg-gray-50 rounded-lg border">
<span class="text-gray-900 font-medium">{{ apiKey.name }}</span>
</div>
</div>
<div v-if="apiKey.description">
<label class="block text-sm font-semibold text-gray-700 mb-2">描述</label>
<p class="text-gray-600 text-sm">{{ apiKey.description }}</p>
<label class="block text-sm font-semibold text-gray-700 mb-2">备注</label>
<div class="p-3 bg-gray-50 rounded-lg border">
<span class="text-gray-700">{{ apiKey.description || '无描述' }}</span>
</div>
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-2">API Key</label>
<div class="relative">
<input
:type="showFullKey ? 'text' : 'password'"
:value="apiKey.key"
readonly
class="form-input w-full pr-24 font-mono text-sm bg-gray-50"
>
<div class="absolute right-1 top-1 flex gap-1">
<div class="p-4 pr-14 bg-gray-900 rounded-lg border font-mono text-sm text-white break-all min-h-[60px] flex items-center">
{{ getDisplayedApiKey() }}
</div>
<div class="absolute top-3 right-3">
<button
@click="toggleKeyVisibility"
type="button"
class="px-3 py-1.5 bg-gray-100 hover:bg-gray-200 rounded-lg text-sm transition-colors"
:title="showFullKey ? '隐藏' : '显示'"
class="btn-icon-sm hover:bg-gray-800 bg-gray-700"
:title="showFullKey ? '隐藏API Key' : '显示完整API Key'"
>
<i :class="showFullKey ? 'fas fa-eye-slash' : 'fas fa-eye'"></i>
</button>
<button
@click="copyApiKey"
type="button"
class="px-3 py-1.5 bg-blue-100 hover:bg-blue-200 text-blue-700 rounded-lg text-sm transition-colors"
title="复制"
>
<i class="fas fa-copy"></i>
{{ copied ? '已复制' : '复制' }}
<i :class="['fas', showFullKey ? 'fa-eye-slash' : 'fa-eye', 'text-gray-300']"></i>
</button>
</div>
</div>
<p class="text-xs text-gray-500 mt-2">
点击眼睛图标切换显示模式使用下方按钮复制完整 API Key
</p>
</div>
</div>
<!-- 使用说明 -->
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
<h4 class="font-semibold text-gray-800 mb-2">
<i class="fas fa-info-circle mr-2 text-blue-500"></i>使用说明
</h4>
<div class="text-sm text-gray-700 space-y-2">
<p>1. HTTP 请求头中添加</p>
<code class="block bg-white rounded px-3 py-2 text-xs">Authorization: Bearer {{ apiKey.key }}</code>
<p class="pt-2">2. 请求示例</p>
<pre class="bg-white rounded px-3 py-2 text-xs overflow-x-auto">curl -X POST {{ currentBaseUrl }}v1/messages \
-H "Authorization: Bearer {{ apiKey.key }}" \
-H "Content-Type: application/json" \
-d '{"model": "claude-3-opus-20240229", "messages": [...]}'</pre>
</div>
</div>
<div class="flex justify-end">
<!-- 操作按钮 -->
<div class="flex gap-3">
<button
@click="$emit('close')"
class="btn btn-primary px-6 py-2.5"
@click="copyApiKey"
class="flex-1 btn btn-primary py-3 px-6 font-semibold flex items-center justify-center gap-2"
>
<i class="fas fa-copy"></i>
复制 API Key
</button>
<button
@click="handleClose"
class="px-6 py-3 bg-gray-100 text-gray-700 rounded-xl font-semibold hover:bg-gray-200 transition-colors"
>
我已保存
</button>
@@ -105,7 +96,7 @@
</template>
<script setup>
import { ref, computed } from 'vue'
import { ref } from 'vue'
import { showToast } from '@/utils/toast'
const props = defineProps({
@@ -118,31 +109,62 @@ const props = defineProps({
const emit = defineEmits(['close'])
const showFullKey = ref(false)
const copied = ref(false)
// 计算基础 URL
const currentBaseUrl = computed(() => {
return `${window.location.protocol}//${window.location.host}/api/`
})
// 切换密钥可见性
const toggleKeyVisibility = () => {
showFullKey.value = !showFullKey.value
}
// 获取显示的API Key
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)
}
}
// 复制 API Key
const copyApiKey = async () => {
const key = props.apiKey.apiKey || props.apiKey.key || ''
if (!key) {
showToast('API Key 不存在', 'error')
return
}
try {
await navigator.clipboard.writeText(props.apiKey.key)
copied.value = true
await navigator.clipboard.writeText(key)
showToast('API Key 已复制到剪贴板', 'success')
// 3秒后重置复制状态
setTimeout(() => {
copied.value = false
}, 3000)
} catch (error) {
showToast('复制失败,请手动复制', 'error')
console.error('Failed to copy:', error)
// 降级方案:创建一个临时文本区域
const textArea = document.createElement('textarea')
textArea.value = key
document.body.appendChild(textArea)
textArea.select()
try {
document.execCommand('copy')
showToast('API Key 已复制到剪贴板', 'success')
} catch (fallbackError) {
showToast('复制失败,请手动复制', 'error')
} finally {
document.body.removeChild(textArea)
}
}
}
// 关闭弹窗
const handleClose = () => {
const confirmed = confirm(
'关闭后将无法再次查看完整的API Key请确保已经妥善保存。\n\n确定要关闭吗'
)
if (confirmed) {
emit('close')
}
}
</script>