Files
ystp/frontend/src/pages/dashboard/DashboardApiKeysPage.vue

237 lines
7.4 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import {
createApiKey,
disableApiKey,
listApiKeys,
rotateApiKey,
type ApiKeyView,
type CreateApiKeyResponse,
} from '@/services/api'
import { ApiError } from '@/services/http'
import { useAuthStore } from '@/stores/auth'
const auth = useAuthStore()
const loading = ref(true)
const error = ref<string | null>(null)
const apiKeys = ref<ApiKeyView[]>([])
const name = ref('')
const creating = ref(false)
const created = ref<CreateApiKeyResponse | null>(null)
const createdContext = ref<'create' | 'rotate' | null>(null)
const copyStatus = ref<string | null>(null)
const rotating = ref<string | null>(null)
async function refresh() {
if (!auth.token) return
error.value = null
try {
const resp = await listApiKeys(auth.token)
apiKeys.value = resp.api_keys
} catch (err) {
if (err instanceof ApiError) {
error.value = `[${err.code}] ${err.message}`
} else {
error.value = '加载失败,请稍后再试'
}
}
}
onMounted(async () => {
await refresh()
loading.value = false
})
async function create() {
if (!auth.token) return
creating.value = true
error.value = null
copyStatus.value = null
try {
const resp = await createApiKey(auth.token, name.value.trim())
created.value = resp
createdContext.value = 'create'
name.value = ''
await refresh()
} catch (err) {
if (err instanceof ApiError) {
error.value = `[${err.code}] ${err.message}`
} else {
error.value = '创建失败,请稍后再试'
}
} finally {
creating.value = false
}
}
async function disable(keyId: string) {
if (!auth.token) return
if (!confirm('确定要禁用这个 Key 吗?')) return
try {
await disableApiKey(auth.token, keyId)
await refresh()
} catch (err) {
if (err instanceof ApiError) {
error.value = `[${err.code}] ${err.message}`
} else {
error.value = '操作失败,请稍后再试'
}
}
}
async function rotate(keyId: string) {
if (!auth.token) return
if (!confirm('确定要轮换这个 Key 吗?旧 Key 将立即失效。')) return
rotating.value = keyId
error.value = null
copyStatus.value = null
try {
const resp = await rotateApiKey(auth.token, keyId)
created.value = resp
createdContext.value = 'rotate'
await refresh()
} catch (err) {
if (err instanceof ApiError) {
error.value = `[${err.code}] ${err.message}`
} else {
error.value = '操作失败,请稍后再试'
}
} finally {
rotating.value = null
}
}
async function copyKey() {
if (!created.value?.key) return
try {
await navigator.clipboard.writeText(created.value.key)
copyStatus.value = '已复制'
} catch {
copyStatus.value = '复制失败'
}
}
function clearCreated() {
created.value = null
createdContext.value = null
copyStatus.value = null
}
</script>
<template>
<div class="space-y-6">
<div class="space-y-1">
<h1 class="text-xl font-semibold text-slate-900">API Keys</h1>
<p class="text-sm text-slate-600"> Pro/Business 可创建创建时只展示一次完整 Key</p>
</div>
<div v-if="error" class="rounded-lg border border-rose-200 bg-rose-50 p-4 text-sm text-rose-900">
{{ error }}
</div>
<div v-if="created" class="rounded-xl border border-emerald-200 bg-emerald-50 p-5 text-sm text-emerald-950">
<div class="font-medium">{{ createdContext === 'rotate' ? '已轮换' : '已创建' }}{{ created.name }}</div>
<div class="mt-2 text-xs text-emerald-900">请保存此 Key它只会显示一次</div>
<pre class="mt-2 overflow-auto rounded-lg bg-slate-950 p-3 text-xs text-slate-100"><code>{{ created.key }}</code></pre>
<div class="mt-3 flex items-center gap-2">
<button
type="button"
class="rounded-md bg-emerald-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-emerald-700"
@click="copyKey"
>
一键复制
</button>
<div v-if="copyStatus" class="text-xs text-emerald-900">{{ copyStatus }}</div>
<button
type="button"
class="ml-auto rounded-md border border-emerald-200 bg-white px-3 py-1.5 text-sm text-emerald-800 hover:bg-emerald-100"
@click="clearCreated"
>
我已保存
</button>
</div>
</div>
<div class="rounded-xl border border-slate-200 bg-white p-5">
<div class="text-sm font-medium text-slate-900">创建 API Key</div>
<div class="mt-3 flex flex-col gap-2 sm:flex-row sm:items-end">
<label class="flex-1 space-y-1">
<div class="text-xs font-medium text-slate-600">名称</div>
<input
v-model="name"
class="w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm text-slate-800"
placeholder="例如 CI / prod / local"
/>
</label>
<button
type="button"
class="rounded-md bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 disabled:opacity-50"
:disabled="creating || !name.trim()"
@click="create"
>
{{ creating ? '创建中…' : '创建' }}
</button>
</div>
</div>
<div class="rounded-xl border border-slate-200 bg-white p-5">
<div class="flex items-center justify-between">
<div class="text-sm font-medium text-slate-900">已有 Keys</div>
<button
type="button"
class="rounded-md border border-slate-200 bg-white px-3 py-1.5 text-sm text-slate-700 hover:bg-slate-50"
@click="refresh"
>
刷新
</button>
</div>
<div v-if="loading" class="mt-3 text-sm text-slate-600">加载中</div>
<div v-else-if="apiKeys.length === 0" class="mt-3 text-sm text-slate-600">暂无</div>
<div v-else class="mt-4 space-y-2">
<div
v-for="k in apiKeys"
:key="k.id"
class="flex flex-col gap-2 rounded-lg border border-slate-200 p-3 sm:flex-row sm:items-center sm:justify-between"
>
<div class="min-w-0">
<div class="truncate text-sm font-medium text-slate-900">{{ k.name }}</div>
<div class="text-xs text-slate-500">
{{ k.key_prefix }} · rate: {{ k.rate_limit }}/min
<span v-if="k.last_used_at"> · 上次使用 {{ new Date(k.last_used_at).toLocaleString() }}</span>
</div>
</div>
<div class="flex items-center gap-2">
<span
class="rounded-full px-2 py-1 text-xs"
:class="k.is_active ? 'bg-emerald-50 text-emerald-700' : 'bg-slate-100 text-slate-600'"
>
{{ k.is_active ? '启用' : '禁用' }}
</span>
<button
v-if="k.is_active"
type="button"
class="rounded-md border border-slate-200 bg-white px-3 py-1.5 text-xs text-slate-700 hover:bg-slate-50"
:disabled="rotating === k.id"
@click="rotate(k.id)"
>
{{ rotating === k.id ? '轮换中…' : '轮换' }}
</button>
<button
v-if="k.is_active"
type="button"
class="rounded-md border border-slate-200 bg-white px-3 py-1.5 text-xs text-slate-700 hover:bg-slate-50"
@click="disable(k.id)"
>
禁用
</button>
</div>
</div>
</div>
</div>
</div>
</template>