ApiKeysView.vue 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293
  1. <script setup lang="ts">
  2. import { onMounted, ref } from 'vue'
  3. import type { ApiKeyDto } from '@agent-server/shared'
  4. import PageHeader from '../components/PageHeader.vue'
  5. import { admin, errorMessage } from '../lib/api'
  6. import { formatTime, toast } from '../lib/ui'
  7. const keys = ref<ApiKeyDto[]>([])
  8. const name = ref('')
  9. const created = ref<{ key: string; name: string } | null>(null)
  10. async function load() {
  11. keys.value = await admin.apiKeys().catch(err => (toast(errorMessage(err), 'error'), []))
  12. }
  13. async function create() {
  14. if (!name.value.trim()) return
  15. try {
  16. const r = await admin.createApiKey(name.value.trim())
  17. created.value = { key: r.key, name: r.apiKey.name }
  18. name.value = ''
  19. await load()
  20. } catch (err) {
  21. toast(errorMessage(err), 'error')
  22. }
  23. }
  24. async function toggle(k: ApiKeyDto) {
  25. await admin.setApiKeyEnabled(k.id, !k.enabled).catch(err => toast(errorMessage(err), 'error'))
  26. await load()
  27. }
  28. async function remove(k: ApiKeyDto) {
  29. if (!confirm(`删除 API Key「${k.name}」?使用它的平台将无法再调用。`)) return
  30. await admin.deleteApiKey(k.id).catch(err => toast(errorMessage(err), 'error'))
  31. await load()
  32. }
  33. async function copy(text: string) {
  34. try {
  35. await navigator.clipboard.writeText(text)
  36. toast('已复制', 'success')
  37. } catch {
  38. toast('复制失败,请手动选择复制', 'error')
  39. }
  40. }
  41. onMounted(load)
  42. </script>
  43. <template>
  44. <div class="h-full flex flex-col">
  45. <PageHeader title="API Keys" subtitle="每个接入的业务平台一个 Key,调用时放在 Authorization: Bearer 头中" />
  46. <div class="flex-1 overflow-y-auto p-6 flex flex-col gap-5 max-w-240">
  47. <form class="flex gap-2" @submit.prevent="create">
  48. <input v-model="name" class="field max-w-80" placeholder="平台名称,例如:家庭记账" aria-label="名称">
  49. <button type="submit" class="btn-primary" :disabled="!name.trim()"><span class="codicon codicon-add" />签发</button>
  50. </form>
  51. <div v-if="created" class="card p-4 flex flex-col gap-2 border-accent">
  52. <div class="font-600">「{{ created.name }}」的 API Key(只显示这一次,请立即保存)</div>
  53. <div class="flex gap-2">
  54. <code class="flex-1 font-mono text-[12.5px] p-2 rounded-md bg-code break-all select-all">{{ created.key }}</code>
  55. <button type="button" class="btn-secondary" @click="copy(created.key)"><span class="codicon codicon-copy" />复制</button>
  56. </div>
  57. <button type="button" class="btn-ghost self-start" @click="created = null">我已保存</button>
  58. </div>
  59. <table class="w-full border-collapse text-[12.5px]">
  60. <thead>
  61. <tr class="text-left text-fg-muted">
  62. <th class="py-2 font-500">名称</th><th class="font-500">前缀</th><th class="font-500">状态</th>
  63. <th class="font-500">创建时间</th><th class="font-500">最近使用</th><th />
  64. </tr>
  65. </thead>
  66. <tbody>
  67. <tr v-for="k in keys" :key="k.id" class="border-t border-t-solid border-border">
  68. <td class="py-2">{{ k.name }}</td>
  69. <td class="font-mono">{{ k.prefix }}…</td>
  70. <td><span class="badge" :class="k.enabled ? 'bg-success text-white' : 'bg-code text-fg-muted'">{{ k.enabled ? '启用' : '停用' }}</span></td>
  71. <td>{{ formatTime(k.createdAt) }}</td>
  72. <td>{{ formatTime(k.lastUsedAt) }}</td>
  73. <td class="text-right whitespace-nowrap">
  74. <button type="button" class="btn-ghost" @click="toggle(k)">{{ k.enabled ? '停用' : '启用' }}</button>
  75. <button type="button" class="icon-btn" title="删除" @click="remove(k)"><span class="codicon codicon-trash" /></button>
  76. </td>
  77. </tr>
  78. <tr v-if="!keys.length"><td colspan="6" class="py-4 hint">还没有 API Key</td></tr>
  79. </tbody>
  80. </table>
  81. </div>
  82. </div>
  83. </template>