mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 18:39:17 +00:00
Revert "feat: 新增AD域控用户认证系统"
This commit is contained in:
@@ -33,31 +33,12 @@
|
||||
>名称</label
|
||||
>
|
||||
<input
|
||||
v-model="form.name"
|
||||
class="form-input w-full text-sm"
|
||||
maxlength="100"
|
||||
placeholder="请输入API Key名称"
|
||||
required
|
||||
class="form-input w-full cursor-not-allowed bg-gray-100 text-sm dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400"
|
||||
disabled
|
||||
type="text"
|
||||
:value="form.name"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400 sm:mt-2">最多100个字符</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
class="mb-1.5 block text-xs font-semibold text-gray-700 dark:text-gray-300 sm:mb-3 sm:text-sm"
|
||||
>描述</label
|
||||
>
|
||||
<textarea
|
||||
v-model="form.description"
|
||||
class="form-input w-full text-sm"
|
||||
maxlength="500"
|
||||
placeholder="请输入API Key描述(可选)"
|
||||
rows="3"
|
||||
></textarea>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400 sm:mt-2">
|
||||
最多500个字符(可选)
|
||||
</p>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400 sm:mt-2">名称不可修改</p>
|
||||
</div>
|
||||
|
||||
<!-- 标签 -->
|
||||
@@ -651,7 +632,6 @@ const unselectedTags = computed(() => {
|
||||
// 表单数据
|
||||
const form = reactive({
|
||||
name: '',
|
||||
description: '',
|
||||
tokenLimit: '',
|
||||
rateLimitWindow: '',
|
||||
rateLimitRequests: '',
|
||||
@@ -727,8 +707,6 @@ const updateApiKey = async () => {
|
||||
try {
|
||||
// 准备提交的数据
|
||||
const data = {
|
||||
name: form.name,
|
||||
description: form.description,
|
||||
tokenLimit:
|
||||
form.tokenLimit !== '' && form.tokenLimit !== null ? parseInt(form.tokenLimit) : 0,
|
||||
rateLimitWindow:
|
||||
@@ -915,7 +893,6 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
form.name = props.apiKey.name
|
||||
form.description = props.apiKey.description || ''
|
||||
form.tokenLimit = props.apiKey.tokenLimit || ''
|
||||
form.rateLimitWindow = props.apiKey.rateLimitWindow || ''
|
||||
form.rateLimitRequests = props.apiKey.rateLimitRequests || ''
|
||||
|
||||
@@ -1,481 +0,0 @@
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<!-- API Key 创建区域 -->
|
||||
<div class="glass-strong rounded-3xl p-6 shadow-xl">
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<i class="fas fa-key text-2xl text-blue-500" />
|
||||
<div>
|
||||
<h3 class="text-xl font-bold text-gray-800 dark:text-gray-100">API Keys 管理</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">每个用户只能创建一个 API Key</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">{{ apiKeys.length }}/1 个 Key</div>
|
||||
</div>
|
||||
|
||||
<!-- 创建新 API Key -->
|
||||
<div v-if="apiKeys.length === 0" class="space-y-4">
|
||||
<div
|
||||
class="rounded-xl border-2 border-dashed border-gray-300 bg-gray-50/50 p-6 text-center dark:border-gray-600 dark:bg-gray-800/50"
|
||||
>
|
||||
<i class="fas fa-plus-circle mb-3 text-3xl text-gray-400" />
|
||||
<h4 class="mb-2 text-lg font-medium text-gray-700 dark:text-gray-300">
|
||||
创建您的第一个 API Key
|
||||
</h4>
|
||||
<p class="mb-4 text-sm text-gray-500 dark:text-gray-400">
|
||||
API Key 将用于访问 Claude Relay Service
|
||||
</p>
|
||||
<form class="mx-auto max-w-md space-y-4" @submit.prevent="createApiKey">
|
||||
<div class="text-center">
|
||||
<p class="mb-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
API Key 名称将自动设置为您的用户名
|
||||
</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-500">使用额度:无限制</p>
|
||||
</div>
|
||||
<button
|
||||
class="w-full rounded-xl bg-gradient-to-r from-blue-500 to-purple-600 px-6 py-3 font-medium text-white transition-all hover:from-blue-600 hover:to-purple-700 disabled:opacity-50"
|
||||
:disabled="createLoading"
|
||||
type="submit"
|
||||
>
|
||||
<div v-if="createLoading" class="flex items-center justify-center gap-2">
|
||||
<div
|
||||
class="h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent"
|
||||
></div>
|
||||
创建中...
|
||||
</div>
|
||||
<div v-else class="flex items-center justify-center gap-2">
|
||||
<i class="fas fa-plus" />
|
||||
创建 API Key
|
||||
</div>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 现有 API Keys 显示 -->
|
||||
<div v-if="apiKeys.length > 0" class="space-y-4">
|
||||
<div
|
||||
v-for="apiKey in apiKeys"
|
||||
:key="apiKey.id"
|
||||
class="glass-strong rounded-3xl p-6 shadow-xl"
|
||||
>
|
||||
<div class="mb-4 flex items-start justify-between">
|
||||
<div class="flex items-start gap-4">
|
||||
<div
|
||||
class="flex h-12 w-12 items-center justify-center rounded-xl bg-gradient-to-br from-blue-500/20 to-purple-500/20"
|
||||
>
|
||||
<i class="fas fa-key text-lg text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="text-lg font-semibold text-gray-800 dark:text-gray-100">
|
||||
{{ apiKey.name || '未命名 API Key' }}
|
||||
</h4>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
创建时间:{{ formatDate(apiKey.createdAt) }}
|
||||
</p>
|
||||
<div class="mt-2 flex items-center justify-between">
|
||||
<span
|
||||
class="inline-flex items-center gap-1 rounded-full px-2 py-1 text-xs font-medium"
|
||||
:class="
|
||||
apiKey.isActive
|
||||
? 'bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-400'
|
||||
: 'bg-red-100 text-red-800 dark:bg-red-900/20 dark:text-red-400'
|
||||
"
|
||||
>
|
||||
<i :class="apiKey.isActive ? 'fas fa-check-circle' : 'fas fa-times-circle'" />
|
||||
{{ apiKey.isActive ? '活跃' : '已禁用' }}
|
||||
</span>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
:class="[
|
||||
apiKey.isActive
|
||||
? 'text-orange-600 hover:bg-orange-50 dark:text-orange-400 dark:hover:bg-orange-900/20'
|
||||
: 'text-green-600 hover:bg-green-50 dark:text-green-400 dark:hover:bg-green-900/20',
|
||||
'rounded-lg px-3 py-1 text-xs font-medium transition-colors'
|
||||
]"
|
||||
@click="toggleApiKeyStatus(apiKey)"
|
||||
>
|
||||
<i :class="['fas mr-1', apiKey.isActive ? 'fa-ban' : 'fa-check-circle']" />
|
||||
{{ apiKey.isActive ? '禁用' : '激活' }}
|
||||
</button>
|
||||
<button
|
||||
class="rounded-lg px-3 py-1 text-xs font-medium text-red-600 transition-colors hover:bg-red-50 dark:text-red-400 dark:hover:bg-red-900/20"
|
||||
@click="deleteApiKey(apiKey)"
|
||||
>
|
||||
<i class="fas fa-trash mr-1" />删除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- API Key 显示 - 历史Key无法显示原始内容 -->
|
||||
<div class="mb-4 space-y-3">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
API Key
|
||||
</label>
|
||||
<div
|
||||
class="rounded-xl border border-amber-300 bg-amber-50 p-4 dark:border-amber-600 dark:bg-amber-900/20"
|
||||
>
|
||||
<div class="flex items-center gap-2 text-amber-800 dark:text-amber-200">
|
||||
<i class="fas fa-info-circle" />
|
||||
<span class="text-sm">
|
||||
已关联的历史API
|
||||
Key无法显示原始内容,仅在创建时可见。如需查看完整Key,请删除原key创建新Key。
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-2 text-xs text-amber-600 dark:text-amber-400">
|
||||
Key ID: {{ apiKey.id }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 使用统计 -->
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||
<div class="rounded-xl bg-blue-50 p-4 dark:bg-blue-900/20">
|
||||
<div class="flex items-center gap-3">
|
||||
<i class="fas fa-chart-bar text-blue-500" />
|
||||
<div>
|
||||
<p class="text-sm font-medium text-blue-800 dark:text-blue-300">今日请求</p>
|
||||
<p class="text-xl font-bold text-blue-900 dark:text-blue-100">
|
||||
{{ apiKey.usage?.daily?.requests?.toLocaleString() || 0 }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-xl bg-green-50 p-4 dark:bg-green-900/20">
|
||||
<div class="flex items-center gap-3">
|
||||
<i class="fas fa-coins text-green-500" />
|
||||
<div>
|
||||
<p class="text-sm font-medium text-green-800 dark:text-green-300">今日Token</p>
|
||||
<p class="text-xl font-bold text-green-900 dark:text-green-100">
|
||||
{{ apiKey.usage?.daily?.tokens?.toLocaleString() || 0 }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-xl bg-purple-50 p-4 dark:bg-purple-900/20">
|
||||
<div class="flex items-center gap-3">
|
||||
<i class="fas fa-dollar-sign text-purple-500" />
|
||||
<div>
|
||||
<p class="text-sm font-medium text-purple-800 dark:text-purple-300">今日费用</p>
|
||||
<p class="text-xl font-bold text-purple-900 dark:text-purple-100">
|
||||
${{ (apiKey.dailyCost || 0).toFixed(4) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Token 额度进度条 -->
|
||||
<div v-if="apiKey.tokenLimit > 0" class="mt-4">
|
||||
<div class="flex items-center justify-between text-sm text-gray-600 dark:text-gray-400">
|
||||
<span>Token 使用进度</span>
|
||||
<span>
|
||||
{{ apiKey.usage?.total?.tokens?.toLocaleString() || 0 }} /
|
||||
{{ apiKey.tokenLimit?.toLocaleString() || 0 }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-2 h-3 overflow-hidden rounded-full bg-gray-200 dark:bg-gray-700">
|
||||
<div
|
||||
class="h-full rounded-full bg-gradient-to-r from-blue-500 to-purple-600 transition-all duration-500"
|
||||
:style="{
|
||||
width: `${Math.min(calculateTokenUsagePercentage(apiKey.usage?.total?.tokens || 0, apiKey.tokenLimit || 0), 100)}%`
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 费用限制进度条 -->
|
||||
<div v-if="apiKey.dailyCostLimit > 0" class="mt-4">
|
||||
<div class="flex items-center justify-between text-sm text-gray-600 dark:text-gray-400">
|
||||
<span>每日费用限制</span>
|
||||
<span>
|
||||
${{ (apiKey.dailyCost || 0).toFixed(4) }} / ${{
|
||||
(apiKey.dailyCostLimit || 0).toFixed(2)
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-2 h-3 overflow-hidden rounded-full bg-gray-200 dark:bg-gray-700">
|
||||
<div
|
||||
class="h-full rounded-full bg-gradient-to-r from-green-500 to-red-500 transition-all duration-500"
|
||||
:style="{
|
||||
width: `${Math.min(calculateCostUsagePercentage(apiKey.dailyCost || 0, apiKey.dailyCostLimit || 0), 100)}%`
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 查看详细统计按钮 -->
|
||||
<div class="mt-4">
|
||||
<button
|
||||
class="flex w-full items-center justify-center gap-2 rounded-lg bg-gradient-to-r from-indigo-500 to-purple-600 px-4 py-2.5 text-sm font-medium text-white transition-all duration-200 hover:from-indigo-600 hover:to-purple-700 hover:shadow-lg"
|
||||
@click="showUsageDetails(apiKey)"
|
||||
>
|
||||
<i class="fas fa-chart-line" />
|
||||
查看详细统计
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loading" class="flex items-center justify-center py-8">
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="h-6 w-6 animate-spin rounded-full border-2 border-blue-500 border-t-transparent"
|
||||
></div>
|
||||
<span class="text-gray-600 dark:text-gray-400">加载中...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 错误提示 -->
|
||||
<div
|
||||
v-if="error"
|
||||
class="rounded-xl border border-red-500/30 bg-red-500/20 p-4 text-center text-red-800 dark:text-red-400"
|
||||
>
|
||||
<i class="fas fa-exclamation-triangle mr-2" />{{ error }}
|
||||
</div>
|
||||
|
||||
<!-- 成功提示 -->
|
||||
<div
|
||||
v-if="successMessage"
|
||||
class="rounded-xl border border-green-500/30 bg-green-500/20 p-4 text-center text-green-800 dark:text-green-400"
|
||||
>
|
||||
<i class="fas fa-check-circle mr-2" />{{ successMessage }}
|
||||
</div>
|
||||
|
||||
<!-- 使用详情模态框 -->
|
||||
<UsageDetailModal
|
||||
:api-key="selectedApiKeyForDetail || {}"
|
||||
:show="showUsageDetailModal"
|
||||
@close="showUsageDetailModal = false"
|
||||
/>
|
||||
|
||||
<!-- 新API Key模态框 -->
|
||||
<NewApiKeyModal
|
||||
v-if="showNewApiKeyModal"
|
||||
:api-key="newApiKeyData"
|
||||
@close="showNewApiKeyModal = false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import UsageDetailModal from '@/components/apikeys/UsageDetailModal.vue'
|
||||
import NewApiKeyModal from '@/components/apikeys/NewApiKeyModal.vue'
|
||||
|
||||
defineProps({
|
||||
userInfo: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
const createLoading = ref(false)
|
||||
const error = ref('')
|
||||
const successMessage = ref('')
|
||||
const apiKeys = ref([])
|
||||
|
||||
// 使用详情模态框相关
|
||||
const showUsageDetailModal = ref(false)
|
||||
const selectedApiKeyForDetail = ref(null)
|
||||
|
||||
// 新API Key模态框相关
|
||||
const showNewApiKeyModal = ref(false)
|
||||
const newApiKeyData = ref(null)
|
||||
|
||||
const newKeyForm = ref({})
|
||||
|
||||
// 获取用户的 API Keys
|
||||
const fetchApiKeys = async () => {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('user_token')
|
||||
const response = await fetch('/admin/ldap/user/api-keys', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
|
||||
const result = await response.json()
|
||||
if (result.success) {
|
||||
apiKeys.value = result.apiKeys
|
||||
} else {
|
||||
error.value = result.message || '获取 API Keys 失败'
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('获取 API Keys 错误:', err)
|
||||
error.value = '网络错误,请重试'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 创建新的 API Key
|
||||
const createApiKey = async () => {
|
||||
createLoading.value = true
|
||||
error.value = ''
|
||||
successMessage.value = ''
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('user_token')
|
||||
const response = await fetch('/admin/ldap/user/api-keys', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
// name和limit字段都由后端自动生成/设置
|
||||
// name: 用户displayName
|
||||
// limit: 0 (无限制)
|
||||
})
|
||||
})
|
||||
|
||||
const result = await response.json()
|
||||
if (result.success) {
|
||||
// 显示新API Key模态框
|
||||
newApiKeyData.value = result.apiKey
|
||||
showNewApiKeyModal.value = true
|
||||
|
||||
// 更新API Keys列表
|
||||
apiKeys.value = [result.apiKey]
|
||||
newKeyForm.value = {}
|
||||
|
||||
// 清除错误信息
|
||||
error.value = ''
|
||||
} else {
|
||||
error.value = result.message || 'API Key 创建失败'
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('创建 API Key 错误:', err)
|
||||
error.value = '网络错误,请重试'
|
||||
} finally {
|
||||
createLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 计算Token使用百分比
|
||||
const calculateTokenUsagePercentage = (used, limit) => {
|
||||
if (!limit || limit === 0) return 0
|
||||
return Math.round((used / limit) * 100)
|
||||
}
|
||||
|
||||
// 计算费用使用百分比
|
||||
const calculateCostUsagePercentage = (used, limit) => {
|
||||
if (!limit || limit === 0) return 0
|
||||
return Math.round((used / limit) * 100)
|
||||
}
|
||||
|
||||
// 切换API Key状态
|
||||
const toggleApiKeyStatus = async (apiKey) => {
|
||||
const action = apiKey.isActive ? '禁用' : '激活'
|
||||
if (confirm(`确定要${action}这个API Key吗?`)) {
|
||||
await updateApiKey(apiKey.id, { isActive: !apiKey.isActive })
|
||||
}
|
||||
}
|
||||
|
||||
// 删除API Key
|
||||
const deleteApiKey = async (apiKey) => {
|
||||
if (confirm(`确定要删除API Key "${apiKey.name}" 吗?删除后将无法恢复!`)) {
|
||||
try {
|
||||
const token = localStorage.getItem('user_token')
|
||||
const response = await fetch(`/admin/ldap/user/api-keys/${apiKey.id}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
|
||||
const result = await response.json()
|
||||
if (result.success) {
|
||||
successMessage.value = 'API Key 删除成功'
|
||||
// 从本地数组中移除
|
||||
const index = apiKeys.value.findIndex((k) => k.id === apiKey.id)
|
||||
if (index > -1) {
|
||||
apiKeys.value.splice(index, 1)
|
||||
}
|
||||
setTimeout(() => {
|
||||
successMessage.value = ''
|
||||
}, 3000)
|
||||
} else {
|
||||
error.value = result.message || 'API Key 删除失败'
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('删除 API Key 错误:', err)
|
||||
error.value = '网络错误,请重试'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新API Key
|
||||
const updateApiKey = async (keyId, updates) => {
|
||||
try {
|
||||
const token = localStorage.getItem('user_token')
|
||||
const response = await fetch(`/admin/ldap/user/api-keys/${keyId}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(updates)
|
||||
})
|
||||
|
||||
const result = await response.json()
|
||||
if (result.success) {
|
||||
successMessage.value = 'API Key 更新成功'
|
||||
// 更新本地数据
|
||||
const apiKey = apiKeys.value.find((k) => k.id === keyId)
|
||||
if (apiKey) {
|
||||
Object.assign(apiKey, updates)
|
||||
}
|
||||
setTimeout(() => {
|
||||
successMessage.value = ''
|
||||
}, 3000)
|
||||
} else {
|
||||
error.value = result.message || 'API Key 更新失败'
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('更新 API Key 错误:', err)
|
||||
error.value = '网络错误,请重试'
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString) return ''
|
||||
const date = new Date(dateString)
|
||||
return date.toLocaleDateString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
}
|
||||
|
||||
// 显示使用详情
|
||||
const showUsageDetails = (apiKey) => {
|
||||
selectedApiKeyForDetail.value = apiKey
|
||||
showUsageDetailModal.value = true
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchApiKeys()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 使用全局样式 */
|
||||
</style>
|
||||
@@ -1,268 +0,0 @@
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<!-- 统计概览卡片 -->
|
||||
<div class="glass-strong rounded-3xl p-6 shadow-xl">
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<i class="fas fa-chart-line text-2xl text-blue-500" />
|
||||
<div>
|
||||
<h3 class="text-xl font-bold text-gray-800 dark:text-gray-100">使用统计</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">您的 API 使用情况概览</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="rounded-xl bg-blue-500 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-600"
|
||||
:disabled="loading"
|
||||
@click="refreshStats"
|
||||
>
|
||||
<i class="fas fa-sync-alt mr-1" :class="{ 'animate-spin': loading }" />
|
||||
刷新
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 统计卡片网格 -->
|
||||
<div v-if="stats" class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
<!-- API Key 数量 -->
|
||||
<div
|
||||
class="rounded-xl bg-gradient-to-br from-blue-500/10 to-blue-600/10 p-4 dark:from-blue-500/5 dark:to-blue-600/5"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-blue-800 dark:text-blue-300">API Keys</p>
|
||||
<p class="text-2xl font-bold text-blue-900 dark:text-blue-100">
|
||||
{{ stats.keyCount }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-full bg-blue-500/20 p-2">
|
||||
<i class="fas fa-key text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 总使用量 -->
|
||||
<div
|
||||
class="rounded-xl bg-gradient-to-br from-green-500/10 to-green-600/10 p-4 dark:from-green-500/5 dark:to-green-600/5"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-green-800 dark:text-green-300">总使用量</p>
|
||||
<p class="text-2xl font-bold text-green-900 dark:text-green-100">
|
||||
{{ stats.totalUsage.toLocaleString() }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-full bg-green-500/20 p-2">
|
||||
<i class="fas fa-chart-bar text-green-600 dark:text-green-400" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 总额度 -->
|
||||
<div
|
||||
class="rounded-xl bg-gradient-to-br from-purple-500/10 to-purple-600/10 p-4 dark:from-purple-500/5 dark:to-purple-600/5"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-purple-800 dark:text-purple-300">总额度</p>
|
||||
<p class="text-2xl font-bold text-purple-900 dark:text-purple-100">
|
||||
{{ stats.totalLimit.toLocaleString() }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-full bg-purple-500/20 p-2">
|
||||
<i class="fas fa-battery-three-quarters text-purple-600 dark:text-purple-400" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 使用率 -->
|
||||
<div
|
||||
class="rounded-xl bg-gradient-to-br from-orange-500/10 to-orange-600/10 p-4 dark:from-orange-500/5 dark:to-orange-600/5"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-orange-800 dark:text-orange-300">使用率</p>
|
||||
<p class="text-2xl font-bold text-orange-900 dark:text-orange-100">
|
||||
{{ stats.percentage }}%
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-full bg-orange-500/20 p-2">
|
||||
<i class="fas fa-percentage text-orange-600 dark:text-orange-400" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loading && !stats" class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
<div
|
||||
v-for="i in 4"
|
||||
:key="i"
|
||||
class="animate-pulse rounded-xl bg-gray-200/50 p-4 dark:bg-gray-700/50"
|
||||
>
|
||||
<div class="space-y-3">
|
||||
<div class="h-4 w-20 rounded bg-gray-300/70 dark:bg-gray-600/70"></div>
|
||||
<div class="h-8 w-16 rounded bg-gray-300/70 dark:bg-gray-600/70"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 各个 API Key 详细统计 -->
|
||||
<div v-if="stats && stats.keys.length > 0" class="space-y-4">
|
||||
<h4 class="text-lg font-semibold text-gray-800 dark:text-gray-100">API Key 详细统计</h4>
|
||||
|
||||
<div
|
||||
v-for="keyStats in stats.keys"
|
||||
:key="keyStats.id"
|
||||
class="glass-strong rounded-2xl p-5 shadow-lg"
|
||||
>
|
||||
<div class="mb-4 flex items-start justify-between">
|
||||
<div class="flex items-start gap-3">
|
||||
<div
|
||||
class="flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-br from-blue-500/20 to-purple-500/20"
|
||||
>
|
||||
<i class="fas fa-key text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="font-semibold text-gray-800 dark:text-gray-100">
|
||||
{{ keyStats.name || '未命名 API Key' }}
|
||||
</h5>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">ID: {{ keyStats.id }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">使用率</p>
|
||||
<p class="text-lg font-bold text-gray-800 dark:text-gray-100">
|
||||
{{ keyStats.percentage }}%
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 使用统计条 -->
|
||||
<div class="mb-3 grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<span class="text-gray-600 dark:text-gray-400">已使用:</span>
|
||||
<span class="font-semibold text-gray-800 dark:text-gray-100">{{
|
||||
keyStats.used.toLocaleString()
|
||||
}}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-gray-600 dark:text-gray-400">总额度:</span>
|
||||
<span class="font-semibold text-gray-800 dark:text-gray-100">{{
|
||||
keyStats.limit.toLocaleString()
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 进度条 -->
|
||||
<div class="relative h-2 overflow-hidden rounded-full bg-gray-200 dark:bg-gray-700">
|
||||
<div
|
||||
class="h-full rounded-full bg-gradient-to-r transition-all duration-500"
|
||||
:class="getProgressColor(keyStats.percentage)"
|
||||
:style="{ width: `${Math.min(keyStats.percentage, 100)}%` }"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<!-- 状态警告 -->
|
||||
<div
|
||||
v-if="keyStats.percentage >= 90"
|
||||
class="mt-3 flex items-center gap-2 text-sm text-red-600 dark:text-red-400"
|
||||
>
|
||||
<i class="fas fa-exclamation-triangle" />
|
||||
<span>额度即将用尽,请注意使用</span>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="keyStats.percentage >= 75"
|
||||
class="mt-3 flex items-center gap-2 text-sm text-orange-600 dark:text-orange-400"
|
||||
>
|
||||
<i class="fas fa-info-circle" />
|
||||
<span>额度使用较多,建议关注使用情况</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 无数据提示 -->
|
||||
<div
|
||||
v-if="!loading && stats && stats.keys.length === 0"
|
||||
class="glass-strong rounded-2xl p-8 text-center shadow-lg"
|
||||
>
|
||||
<i class="fas fa-chart-line mb-3 text-4xl text-gray-400" />
|
||||
<h4 class="mb-2 text-lg font-medium text-gray-700 dark:text-gray-300">暂无使用数据</h4>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
创建 API Key 后开始使用即可查看详细统计
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 错误提示 -->
|
||||
<div
|
||||
v-if="error"
|
||||
class="rounded-xl border border-red-500/30 bg-red-500/20 p-4 text-center text-red-800 dark:text-red-400"
|
||||
>
|
||||
<i class="fas fa-exclamation-triangle mr-2" />{{ error }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
defineProps({
|
||||
userInfo: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
const stats = ref(null)
|
||||
|
||||
// 获取用户统计数据
|
||||
const fetchStats = async () => {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('user_token')
|
||||
const response = await fetch('/admin/ldap/user/usage-stats', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
|
||||
const result = await response.json()
|
||||
if (result.success) {
|
||||
stats.value = result.stats
|
||||
} else {
|
||||
error.value = result.message || '获取统计数据失败'
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('获取统计数据错误:', err)
|
||||
error.value = '网络错误,请重试'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新统计数据
|
||||
const refreshStats = () => {
|
||||
fetchStats()
|
||||
}
|
||||
|
||||
// 获取进度条颜色
|
||||
const getProgressColor = (percentage) => {
|
||||
if (percentage >= 90) return 'from-red-500 to-red-600'
|
||||
if (percentage >= 75) return 'from-orange-500 to-orange-600'
|
||||
if (percentage >= 50) return 'from-yellow-500 to-yellow-600'
|
||||
return 'from-green-500 to-green-600'
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchStats()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 使用全局样式 */
|
||||
</style>
|
||||
@@ -11,7 +11,6 @@ const AccountsView = () => import('@/views/AccountsView.vue')
|
||||
const TutorialView = () => import('@/views/TutorialView.vue')
|
||||
const SettingsView = () => import('@/views/SettingsView.vue')
|
||||
const ApiStatsView = () => import('@/views/ApiStatsView.vue')
|
||||
const UserDashboardView = () => import('@/views/UserDashboardView.vue')
|
||||
|
||||
const routes = [
|
||||
{
|
||||
@@ -42,12 +41,6 @@ const routes = [
|
||||
component: ApiStatsView,
|
||||
meta: { requiresAuth: false }
|
||||
},
|
||||
{
|
||||
path: '/user-dashboard',
|
||||
name: 'UserDashboard',
|
||||
component: UserDashboardView,
|
||||
meta: { requiresAuth: false, userAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/dashboard',
|
||||
component: MainLayout,
|
||||
@@ -140,18 +133,7 @@ router.beforeEach((to, from, next) => {
|
||||
// API Stats 页面不需要认证,直接放行
|
||||
if (to.path === '/api-stats' || to.path.startsWith('/api-stats')) {
|
||||
next()
|
||||
}
|
||||
// 用户仪表盘需要用户token验证
|
||||
else if (to.meta.userAuth) {
|
||||
const userToken = localStorage.getItem('user_token')
|
||||
if (!userToken) {
|
||||
next('/api-stats')
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
}
|
||||
// 管理员页面需要管理员认证
|
||||
else if (to.meta.requiresAuth && !authStore.isAuthenticated) {
|
||||
} else if (to.meta.requiresAuth && !authStore.isAuthenticated) {
|
||||
next('/login')
|
||||
} else if (to.path === '/login' && authStore.isAuthenticated) {
|
||||
next('/dashboard')
|
||||
|
||||
@@ -20,15 +20,6 @@
|
||||
class="h-8 w-px bg-gradient-to-b from-transparent via-gray-300 to-transparent opacity-50 dark:via-gray-600"
|
||||
/>
|
||||
|
||||
<!-- 用户登录按钮 -->
|
||||
<button
|
||||
class="user-login-button flex items-center gap-2 rounded-2xl px-4 py-2 transition-all duration-300 md:px-5 md:py-2.5"
|
||||
@click="showUserLogin"
|
||||
>
|
||||
<i class="fas fa-user-circle text-sm md:text-base" />
|
||||
<span class="text-xs font-semibold tracking-wide md:text-sm">用户登录</span>
|
||||
</button>
|
||||
|
||||
<!-- 管理后台按钮 -->
|
||||
<router-link
|
||||
class="admin-button-refined flex items-center gap-2 rounded-2xl px-4 py-2 transition-all duration-300 md:px-5 md:py-2.5"
|
||||
@@ -138,74 +129,6 @@
|
||||
<TutorialView />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 用户登录模态框 -->
|
||||
<div v-if="showLoginModal" class="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
<div class="fixed inset-0 bg-black/50 backdrop-blur-sm" @click="hideUserLogin"></div>
|
||||
<div class="glass-strong relative w-full max-w-md rounded-2xl p-6 shadow-2xl">
|
||||
<div class="mb-6 text-center">
|
||||
<h2 class="mb-2 text-2xl font-bold text-gray-800 dark:text-gray-100">AD域控登录</h2>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">使用您的域账号登录</p>
|
||||
</div>
|
||||
|
||||
<form class="space-y-4" @submit.prevent="handleUserLogin">
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||
用户名
|
||||
</label>
|
||||
<input
|
||||
v-model="userLoginForm.username"
|
||||
class="w-full rounded-xl border border-gray-300 bg-white/70 px-4 py-3 text-gray-800 placeholder-gray-500 backdrop-blur-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-600 dark:bg-gray-800/70 dark:text-gray-200 dark:placeholder-gray-400"
|
||||
placeholder="请输入域用户名"
|
||||
required
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||
密码
|
||||
</label>
|
||||
<input
|
||||
v-model="userLoginForm.password"
|
||||
class="w-full rounded-xl border border-gray-300 bg-white/70 px-4 py-3 text-gray-800 placeholder-gray-500 backdrop-blur-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-600 dark:bg-gray-800/70 dark:text-gray-200 dark:placeholder-gray-400"
|
||||
placeholder="请输入域密码"
|
||||
required
|
||||
type="password"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3 pt-4">
|
||||
<button
|
||||
class="flex-1 rounded-xl border border-gray-300 bg-white/70 px-4 py-3 text-sm font-medium text-gray-700 backdrop-blur-sm transition-colors hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-800/70 dark:text-gray-300 dark:hover:bg-gray-700"
|
||||
type="button"
|
||||
@click="hideUserLogin"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
class="flex flex-1 items-center justify-center gap-2 rounded-xl bg-gradient-to-r from-blue-500 to-purple-600 px-4 py-3 text-sm font-medium text-white backdrop-blur-sm transition-all hover:from-blue-600 hover:to-purple-700 disabled:opacity-50"
|
||||
:disabled="userLoginLoading"
|
||||
type="submit"
|
||||
>
|
||||
<div
|
||||
v-if="userLoginLoading"
|
||||
class="h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent"
|
||||
></div>
|
||||
<i v-else class="fas fa-sign-in-alt"></i>
|
||||
{{ userLoginLoading ? '登录中...' : '登录' }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div
|
||||
v-if="userLoginError"
|
||||
class="mt-4 rounded-xl border border-red-500/30 bg-red-500/20 p-3 text-center text-sm text-red-800 dark:text-red-400"
|
||||
>
|
||||
<i class="fas fa-exclamation-triangle mr-2"></i>{{ userLoginError }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -234,15 +157,6 @@ const currentTab = ref('stats')
|
||||
// 主题相关
|
||||
const isDarkMode = computed(() => themeStore.isDarkMode)
|
||||
|
||||
// 用户登录相关
|
||||
const showLoginModal = ref(false)
|
||||
const userLoginLoading = ref(false)
|
||||
const userLoginError = ref('')
|
||||
const userLoginForm = ref({
|
||||
username: '',
|
||||
password: ''
|
||||
})
|
||||
|
||||
const {
|
||||
apiKey,
|
||||
apiId,
|
||||
@@ -257,63 +171,6 @@ const {
|
||||
|
||||
const { queryStats, switchPeriod, loadStatsWithApiId, loadOemSettings, reset } = apiStatsStore
|
||||
|
||||
// 用户登录相关方法
|
||||
const showUserLogin = () => {
|
||||
showLoginModal.value = true
|
||||
userLoginError.value = ''
|
||||
userLoginForm.value = {
|
||||
username: '',
|
||||
password: ''
|
||||
}
|
||||
}
|
||||
|
||||
const hideUserLogin = () => {
|
||||
showLoginModal.value = false
|
||||
userLoginError.value = ''
|
||||
userLoginForm.value = {
|
||||
username: '',
|
||||
password: ''
|
||||
}
|
||||
}
|
||||
|
||||
const handleUserLogin = async () => {
|
||||
if (!userLoginForm.value.username || !userLoginForm.value.password) {
|
||||
userLoginError.value = '请输入用户名和密码'
|
||||
return
|
||||
}
|
||||
|
||||
userLoginLoading.value = true
|
||||
userLoginError.value = ''
|
||||
|
||||
try {
|
||||
const response = await fetch('/admin/ldap/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(userLoginForm.value)
|
||||
})
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
if (result.success) {
|
||||
// 保存token到localStorage
|
||||
localStorage.setItem('user_token', result.token)
|
||||
localStorage.setItem('user_info', JSON.stringify(result.user))
|
||||
|
||||
// 跳转到用户专用页面
|
||||
window.location.href = '/admin-next/user-dashboard'
|
||||
} else {
|
||||
userLoginError.value = result.message || '登录失败'
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('用户登录错误:', error)
|
||||
userLoginError.value = '网络错误,请重试'
|
||||
} finally {
|
||||
userLoginLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 处理键盘快捷键
|
||||
const handleKeyDown = (event) => {
|
||||
// Ctrl/Cmd + Enter 查询
|
||||
@@ -452,55 +309,6 @@ watch(apiKey, (newValue) => {
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
|
||||
/* 用户登录按钮 */
|
||||
.user-login-button {
|
||||
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
box-shadow:
|
||||
0 4px 12px rgba(16, 185, 129, 0.25),
|
||||
inset 0 1px 1px rgba(255, 255, 255, 0.2);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 暗色模式下的用户登录按钮 */
|
||||
:global(.dark) .user-login-button {
|
||||
background: rgba(34, 197, 94, 0.8);
|
||||
border: 1px solid rgba(107, 114, 128, 0.4);
|
||||
color: #f3f4f6;
|
||||
box-shadow:
|
||||
0 4px 12px rgba(0, 0, 0, 0.3),
|
||||
inset 0 1px 1px rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.user-login-button:hover {
|
||||
transform: translateY(-2px) scale(1.02);
|
||||
background: linear-gradient(135deg, #059669 0%, #10b981 100%);
|
||||
box-shadow:
|
||||
0 8px 20px rgba(5, 150, 105, 0.35),
|
||||
inset 0 1px 1px rgba(255, 255, 255, 0.3);
|
||||
border-color: rgba(255, 255, 255, 0.4);
|
||||
color: white;
|
||||
}
|
||||
|
||||
:global(.dark) .user-login-button:hover {
|
||||
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
||||
border-color: rgba(34, 197, 94, 0.4);
|
||||
box-shadow:
|
||||
0 8px 20px rgba(16, 185, 129, 0.3),
|
||||
inset 0 1px 1px rgba(255, 255, 255, 0.1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.user-login-button:active {
|
||||
transform: translateY(-1px) scale(1);
|
||||
}
|
||||
|
||||
/* 管理后台按钮 - 精致版本 */
|
||||
.admin-button-refined {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
|
||||
@@ -1,344 +0,0 @@
|
||||
<template>
|
||||
<div class="min-h-screen p-4 md:p-6" :class="isDarkMode ? 'gradient-bg-dark' : 'gradient-bg'">
|
||||
<!-- 顶部导航 -->
|
||||
<div class="glass-strong mb-6 rounded-3xl p-4 shadow-xl md:mb-8 md:p-6">
|
||||
<div class="flex flex-col items-center justify-between gap-4 md:flex-row">
|
||||
<div class="flex items-center gap-4">
|
||||
<LogoTitle
|
||||
:loading="false"
|
||||
logo-src="/assets/logo.png"
|
||||
:subtitle="`欢迎,${userInfo.displayName || userInfo.username}`"
|
||||
title="Claude Relay Service"
|
||||
/>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400">
|
||||
<i class="fas fa-building mr-1"></i
|
||||
>{{
|
||||
userInfo.groups && userInfo.groups.length > 0
|
||||
? extractGroupName(userInfo.groups[0])
|
||||
: '未知部门'
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 md:gap-4">
|
||||
<!-- 主题切换按钮 -->
|
||||
<ThemeToggle mode="dropdown" />
|
||||
|
||||
<!-- 分隔线 -->
|
||||
<div
|
||||
class="h-8 w-px bg-gradient-to-b from-transparent via-gray-300 to-transparent opacity-50 dark:via-gray-600"
|
||||
/>
|
||||
|
||||
<!-- 退出登录按钮 -->
|
||||
<button
|
||||
class="logout-button flex items-center gap-2 rounded-2xl px-4 py-2 transition-all duration-300 md:px-5 md:py-2.5"
|
||||
@click="handleLogout"
|
||||
>
|
||||
<i class="fas fa-sign-out-alt text-sm md:text-base" />
|
||||
<span class="text-xs font-semibold tracking-wide md:text-sm">退出登录</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab 切换 -->
|
||||
<div class="mb-6 md:mb-8">
|
||||
<div class="flex justify-center">
|
||||
<div
|
||||
class="inline-flex w-full max-w-2xl rounded-full border border-white/20 bg-white/10 p-1 shadow-lg backdrop-blur-xl md:w-auto"
|
||||
>
|
||||
<button
|
||||
:class="['tab-pill-button', currentTab === 'api-keys' ? 'active' : '']"
|
||||
@click="currentTab = 'api-keys'"
|
||||
>
|
||||
<i class="fas fa-key mr-1 md:mr-2" />
|
||||
<span class="text-sm md:text-base">API Keys 管理</span>
|
||||
</button>
|
||||
<button
|
||||
:class="['tab-pill-button', currentTab === 'tutorial' ? 'active' : '']"
|
||||
@click="currentTab = 'tutorial'"
|
||||
>
|
||||
<i class="fas fa-graduation-cap mr-1 md:mr-2" />
|
||||
<span class="text-sm md:text-base">使用教程</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- API Keys 管理 -->
|
||||
<div v-if="currentTab === 'api-keys'" class="tab-content">
|
||||
<UserApiKeysView :user-info="userInfo" />
|
||||
</div>
|
||||
|
||||
<!-- 使用教程 -->
|
||||
<div v-if="currentTab === 'tutorial'" class="tab-content">
|
||||
<div class="glass-strong rounded-3xl shadow-xl">
|
||||
<TutorialView />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { useThemeStore } from '@/stores/theme'
|
||||
import LogoTitle from '@/components/common/LogoTitle.vue'
|
||||
import ThemeToggle from '@/components/common/ThemeToggle.vue'
|
||||
import TutorialView from './TutorialView.vue'
|
||||
import UserApiKeysView from '@/components/user/UserApiKeysView.vue'
|
||||
const themeStore = useThemeStore()
|
||||
|
||||
// 当前标签页
|
||||
const currentTab = ref('api-keys')
|
||||
|
||||
// 主题相关
|
||||
const isDarkMode = computed(() => themeStore.isDarkMode)
|
||||
|
||||
// 用户信息
|
||||
const userInfo = ref({})
|
||||
|
||||
// 从组名中提取部门名称
|
||||
const extractGroupName = (group) => {
|
||||
if (!group) return '未知部门'
|
||||
// 从 "CN=总裁办,OU=微店,DC=corp,DC=weidian-inc,DC=com" 中提取 "总裁办"
|
||||
const match = group.match(/CN=([^,]+)/)
|
||||
return match ? match[1] : '未知部门'
|
||||
}
|
||||
|
||||
// 退出登录
|
||||
const handleLogout = () => {
|
||||
localStorage.removeItem('user_token')
|
||||
localStorage.removeItem('user_info')
|
||||
window.location.href = '/admin-next/api-stats'
|
||||
}
|
||||
|
||||
// 验证用户token
|
||||
const verifyToken = async () => {
|
||||
const token = localStorage.getItem('user_token')
|
||||
if (!token) {
|
||||
window.location.href = '/admin-next/api-stats'
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/admin/ldap/verify-token', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
|
||||
const result = await response.json()
|
||||
if (result.success) {
|
||||
userInfo.value = result.user
|
||||
return true
|
||||
} else {
|
||||
localStorage.removeItem('user_token')
|
||||
localStorage.removeItem('user_info')
|
||||
window.location.href = '/admin-next/api-stats'
|
||||
return false
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Token验证失败:', error)
|
||||
localStorage.removeItem('user_token')
|
||||
localStorage.removeItem('user_info')
|
||||
window.location.href = '/admin-next/api-stats'
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(async () => {
|
||||
// 初始化主题
|
||||
themeStore.initTheme()
|
||||
|
||||
// 验证token
|
||||
const isValid = await verifyToken()
|
||||
if (!isValid) return
|
||||
|
||||
// 从localStorage获取用户信息作为备份
|
||||
const storedUserInfo = localStorage.getItem('user_info')
|
||||
if (storedUserInfo && !userInfo.value.username) {
|
||||
userInfo.value = JSON.parse(storedUserInfo)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 渐变背景 */
|
||||
.gradient-bg {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
|
||||
background-attachment: fixed;
|
||||
min-height: 100vh;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.gradient-bg-dark {
|
||||
background: linear-gradient(135deg, #1e293b 0%, #334155 50%, #475569 100%);
|
||||
background-attachment: fixed;
|
||||
min-height: 100vh;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.gradient-bg::before,
|
||||
.gradient-bg-dark::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.gradient-bg::before {
|
||||
background:
|
||||
radial-gradient(circle at 20% 80%, rgba(240, 147, 251, 0.2) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 20%, rgba(102, 126, 234, 0.2) 0%, transparent 50%),
|
||||
radial-gradient(circle at 40% 40%, rgba(118, 75, 162, 0.1) 0%, transparent 50%);
|
||||
}
|
||||
|
||||
.gradient-bg-dark::before {
|
||||
background:
|
||||
radial-gradient(circle at 20% 80%, rgba(100, 116, 139, 0.1) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 20%, rgba(71, 85, 105, 0.1) 0%, transparent 50%),
|
||||
radial-gradient(circle at 40% 40%, rgba(30, 41, 59, 0.1) 0%, transparent 50%);
|
||||
}
|
||||
|
||||
/* 玻璃态效果 */
|
||||
.glass-strong {
|
||||
background: var(--glass-strong-color);
|
||||
backdrop-filter: blur(25px);
|
||||
border: 1px solid var(--border-color);
|
||||
box-shadow:
|
||||
0 25px 50px -12px rgba(0, 0, 0, 0.25),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.05),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
:global(.dark) .glass-strong {
|
||||
box-shadow:
|
||||
0 25px 50px -12px rgba(0, 0, 0, 0.7),
|
||||
0 0 0 1px rgba(55, 65, 81, 0.3),
|
||||
inset 0 1px 0 rgba(75, 85, 99, 0.2);
|
||||
}
|
||||
|
||||
/* 退出登录按钮 */
|
||||
.logout-button {
|
||||
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
box-shadow:
|
||||
0 4px 12px rgba(239, 68, 68, 0.25),
|
||||
inset 0 1px 1px rgba(255, 255, 255, 0.2);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:global(.dark) .logout-button {
|
||||
background: rgba(239, 68, 68, 0.8);
|
||||
border: 1px solid rgba(107, 114, 128, 0.4);
|
||||
color: #f3f4f6;
|
||||
box-shadow:
|
||||
0 4px 12px rgba(0, 0, 0, 0.3),
|
||||
inset 0 1px 1px rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.logout-button:hover {
|
||||
transform: translateY(-2px) scale(1.02);
|
||||
background: linear-gradient(135deg, #dc2626 0%, #ef4444 100%);
|
||||
box-shadow:
|
||||
0 8px 20px rgba(220, 38, 38, 0.35),
|
||||
inset 0 1px 1px rgba(255, 255, 255, 0.3);
|
||||
border-color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
:global(.dark) .logout-button:hover {
|
||||
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
||||
border-color: rgba(239, 68, 68, 0.4);
|
||||
box-shadow:
|
||||
0 8px 20px rgba(239, 68, 68, 0.3),
|
||||
inset 0 1px 1px rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.logout-button:active {
|
||||
transform: translateY(-1px) scale(1);
|
||||
}
|
||||
|
||||
/* Tab 胶囊按钮样式 */
|
||||
.tab-pill-button {
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 9999px;
|
||||
font-weight: 500;
|
||||
font-size: 0.875rem;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
:global(html.dark) .tab-pill-button {
|
||||
color: rgba(209, 213, 219, 0.8);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.tab-pill-button {
|
||||
padding: 0.625rem 1.25rem;
|
||||
flex: none;
|
||||
}
|
||||
}
|
||||
|
||||
.tab-pill-button:hover {
|
||||
color: white;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
:global(html.dark) .tab-pill-button:hover {
|
||||
color: #f3f4f6;
|
||||
background: rgba(100, 116, 139, 0.2);
|
||||
}
|
||||
|
||||
.tab-pill-button.active {
|
||||
background: white;
|
||||
color: #764ba2;
|
||||
box-shadow:
|
||||
0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
||||
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
:global(html.dark) .tab-pill-button.active {
|
||||
background: rgba(71, 85, 105, 0.9);
|
||||
color: #f3f4f6;
|
||||
box-shadow:
|
||||
0 4px 6px -1px rgba(0, 0, 0, 0.3),
|
||||
0 2px 4px -1px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* Tab 内容切换动画 */
|
||||
.tab-content {
|
||||
animation: tabFadeIn 0.4s ease-out;
|
||||
}
|
||||
|
||||
@keyframes tabFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user