feat: 完善管理界面功能和用户体验

- 添加 API Key 窗口倒计时组件 (WindowCountdown)
- 添加自定义下拉菜单组件 (CustomDropdown)
- 优化账户和 API Key 管理界面交互
- 改进教程页面布局和说明文字
- 完善账户状态显示和错误处理

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
shaw
2025-08-08 14:14:46 +08:00
parent 4adc8d9695
commit 7e1a9daa6b
9 changed files with 881 additions and 190 deletions

View File

@@ -7,35 +7,71 @@
<p class="text-sm text-gray-600 sm:text-base">管理和监控您的 API 密钥</p>
</div>
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<!-- Token统计时间范围选择 -->
<div class="flex flex-wrap items-center gap-2">
<select
v-model="apiKeyStatsTimeRange"
class="rounded-md border border-gray-200 bg-white px-3 py-2 text-sm text-gray-700 transition-colors hover:border-gray-300 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-blue-500"
@change="loadApiKeys()"
>
<option value="today">今日</option>
<option value="7days">最近7天</option>
<option value="monthly">本月</option>
<option value="all">全部时间</option>
</select>
<!-- 筛选器组 -->
<div class="flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-center sm:gap-3">
<!-- 时间范围筛选 -->
<div class="group relative min-w-[140px]">
<div
class="absolute -inset-0.5 rounded-lg bg-gradient-to-r from-blue-500 to-purple-500 opacity-0 blur transition duration-300 group-hover:opacity-20"
></div>
<CustomDropdown
v-model="apiKeyStatsTimeRange"
icon="fa-calendar-alt"
icon-color="text-blue-500"
:options="timeRangeOptions"
placeholder="选择时间范围"
@change="loadApiKeys()"
/>
</div>
<!-- 标签筛选器 -->
<select
v-model="selectedTagFilter"
class="rounded-md border border-gray-200 bg-white px-3 py-2 text-sm text-gray-700 transition-colors hover:border-gray-300 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-blue-500"
@change="currentPage = 1"
<div class="group relative min-w-[140px]">
<div
class="absolute -inset-0.5 rounded-lg bg-gradient-to-r from-purple-500 to-pink-500 opacity-0 blur transition duration-300 group-hover:opacity-20"
></div>
<div class="relative">
<CustomDropdown
v-model="selectedTagFilter"
icon="fa-tags"
icon-color="text-purple-500"
:options="tagOptions"
placeholder="所有标签"
@change="currentPage = 1"
/>
<span
v-if="selectedTagFilter"
class="absolute -right-2 -top-2 z-10 flex h-5 w-5 items-center justify-center rounded-full bg-purple-500 text-xs text-white shadow-sm"
>
{{ selectedTagCount }}
</span>
</div>
</div>
<!-- 刷新按钮 -->
<button
class="group relative flex items-center justify-center gap-2 rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm transition-all duration-200 hover:border-gray-300 hover:shadow-md disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto"
:disabled="apiKeysLoading"
@click="loadApiKeys()"
>
<option value="">所有标签</option>
<option v-for="tag in availableTags" :key="tag" :value="tag">
{{ tag }}
</option>
</select>
<div
class="absolute -inset-0.5 rounded-lg bg-gradient-to-r from-green-500 to-teal-500 opacity-0 blur transition duration-300 group-hover:opacity-20"
></div>
<i
:class="[
'fas relative text-green-500',
apiKeysLoading ? 'fa-spinner fa-spin' : 'fa-sync-alt'
]"
/>
<span class="relative">刷新</span>
</button>
</div>
<!-- 创建按钮 -->
<button
class="btn btn-primary flex w-full items-center justify-center gap-2 px-4 py-2 text-sm sm:w-auto"
class="flex w-full items-center justify-center gap-2 rounded-lg bg-gradient-to-r from-blue-500 to-blue-600 px-5 py-2.5 text-sm font-medium text-white shadow-md transition-all duration-200 hover:from-blue-600 hover:to-blue-700 hover:shadow-lg sm:w-auto"
@click.stop="openCreateApiKeyModal"
>
<i class="fas fa-plus" />创建新 Key
<i class="fas fa-plus"></i>
<span>创建新 Key</span>
</button>
</div>
</div>
@@ -368,6 +404,21 @@
<i class="fas fa-clock" />
<span class="ml-1 hidden xl:inline">续期</span>
</button>
<button
:class="[
key.isActive
? 'text-orange-600 hover:bg-orange-50 hover:text-orange-900'
: 'text-green-600 hover:bg-green-50 hover:text-green-900',
'rounded px-2 py-1 text-xs font-medium transition-colors'
]"
:title="key.isActive ? '禁用' : '激活'"
@click="toggleApiKeyStatus(key)"
>
<i :class="['fas', key.isActive ? 'fa-ban' : 'fa-check-circle']" />
<span class="ml-1 hidden xl:inline">{{
key.isActive ? '禁用' : '激活'
}}</span>
</button>
<button
class="rounded px-2 py-1 text-xs font-medium text-red-600 transition-colors hover:bg-red-50 hover:text-red-900"
title="删除"
@@ -801,6 +852,18 @@
<i class="fas fa-clock mr-1" />
续期
</button>
<button
:class="[
key.isActive
? 'bg-orange-50 text-orange-600 hover:bg-orange-100'
: 'bg-green-50 text-green-600 hover:bg-green-100',
'rounded-lg px-3 py-2 text-xs transition-colors'
]"
@click="toggleApiKeyStatus(key)"
>
<i :class="['fas', key.isActive ? 'fa-ban' : 'fa-check-circle', 'mr-1']" />
{{ key.isActive ? '禁用' : '激活' }}
</button>
<button
class="rounded-lg bg-red-50 px-3 py-2 text-xs text-red-600 transition-colors hover:bg-red-100"
@click="deleteApiKey(key.id)"
@@ -964,6 +1027,7 @@ import BatchApiKeyModal from '@/components/apikeys/BatchApiKeyModal.vue'
import ExpiryEditModal from '@/components/apikeys/ExpiryEditModal.vue'
import UsageDetailModal from '@/components/apikeys/UsageDetailModal.vue'
import WindowCountdown from '@/components/apikeys/WindowCountdown.vue'
import CustomDropdown from '@/components/common/CustomDropdown.vue'
// 响应式数据
const clientsStore = useClientsStore()
@@ -986,6 +1050,28 @@ const selectedApiKeyForDetail = ref(null)
const selectedTagFilter = ref('')
const availableTags = ref([])
// 下拉选项数据
const timeRangeOptions = ref([
{ value: 'today', label: '今日', icon: 'fa-clock' },
{ value: '7days', label: '最近7天', icon: 'fa-calendar-week' },
{ value: 'monthly', label: '本月', icon: 'fa-calendar' },
{ value: 'all', label: '全部时间', icon: 'fa-infinity' }
])
const tagOptions = computed(() => {
const options = [{ value: '', label: '所有标签', icon: 'fa-asterisk' }]
availableTags.value.forEach((tag) => {
options.push({ value: tag, label: tag, icon: 'fa-tag' })
})
return options
})
const selectedTagCount = computed(() => {
if (!selectedTagFilter.value) return 0
return apiKeys.value.filter((key) => key.tags && key.tags.includes(selectedTagFilter.value))
.length
})
// 分页相关
const currentPage = ref(1)
const pageSize = ref(10)
@@ -1455,6 +1541,49 @@ const handleRenewSuccess = () => {
loadApiKeys()
}
// 切换API Key状态激活/禁用)
const toggleApiKeyStatus = async (key) => {
let confirmed = true
// 禁用时需要二次确认
if (key.isActive) {
if (window.showConfirm) {
confirmed = await window.showConfirm(
'禁用 API Key',
`确定要禁用 API Key "${key.name}" 吗?禁用后所有使用此 Key 的请求将返回 401 错误。`,
'确定禁用',
'取消'
)
} else {
// 降级方案
confirmed = confirm(
`确定要禁用 API Key "${key.name}" 禁用后所有使用此 Key 的请求将返回 401 错误`
)
}
}
if (!confirmed) return
try {
const data = await apiClient.put(`/admin/api-keys/${key.id}`, {
isActive: !key.isActive
})
if (data.success) {
showToast(`API Key ${key.isActive ? '禁用' : '激活'}`, 'success')
// 更新本地数据
const localKey = apiKeys.value.find((k) => k.id === key.id)
if (localKey) {
localKey.isActive = !key.isActive
}
} else {
showToast(data.message || '操作失败', 'error')
}
} catch (error) {
showToast('操作失败', 'error')
}
}
// 删除API Key
const deleteApiKey = async (keyId) => {
let confirmed = false