mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-06 08:06:19 +00:00
refactor(web): align accounts and api-keys view organization
Extract AccountFilters to mirror the ApiKeyFilters/UsageFilters toolbar convention, move the CCSwitch import and service-URL orchestration out of api-keys/index.vue into a useApiKeyUse composable, adopt useAsyncAction for the admin API key mutations and useIdSet for the connection-test id tracking (both already the sibling-view convention), and drop the unused store barrel re-exports. Markup moved verbatim; built CSS unchanged. Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,3 @@ import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
|
||||
export const pinia = createPinia()
|
||||
|
||||
pinia.use(piniaPluginPersistedstate)
|
||||
|
||||
// 导出所有 store
|
||||
export * from './modules/auth'
|
||||
export * from './modules/ui'
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
<script setup lang="ts">
|
||||
import { Download, Search, Trash2, Upload } from '@lucide/vue'
|
||||
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
import BaseInput from '@/components/base/BaseInput.vue'
|
||||
import BaseSelect from '@/components/base/BaseSelect.vue'
|
||||
import ProviderFilterSegmented from '@/components/ProviderFilterSegmented.vue'
|
||||
import { accountStatusFilterOptions } from '../constants'
|
||||
|
||||
defineProps<{
|
||||
selectedCount: number
|
||||
batchDeleting: boolean
|
||||
exportingAccounts: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
deleteSelected: []
|
||||
exportSelected: []
|
||||
create: []
|
||||
}>()
|
||||
|
||||
const search = defineModel<string>('search', { required: true })
|
||||
const status = defineModel<string>('status', { required: true })
|
||||
const provider = defineModel<string>('provider', { required: true })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex w-full flex-col gap-3 md:flex-row md:flex-wrap md:items-center"
|
||||
role="group"
|
||||
aria-label="账号筛选与操作"
|
||||
>
|
||||
<div class="flex min-w-0 items-center gap-2 md:flex-none md:gap-3">
|
||||
<BaseInput
|
||||
v-model="search"
|
||||
placeholder="搜索邮箱或 ID..."
|
||||
class="min-w-0 flex-1 [--cp-input-current-bg:var(--cp-input-soft-bg)] [--cp-input-current-bg-hover:var(--cp-input-soft-bg-hover)] md:w-80 md:flex-none"
|
||||
>
|
||||
<template #prefix>
|
||||
<Search class="size-4.5 text-(--cp-text-tertiary)" />
|
||||
</template>
|
||||
</BaseInput>
|
||||
|
||||
<BaseSelect
|
||||
v-model="status"
|
||||
:options="accountStatusFilterOptions"
|
||||
aria-label="按账号状态筛选"
|
||||
class="w-34 shrink-0 [--cp-input-current-bg:var(--cp-input-soft-bg)] [--cp-input-current-bg-hover:var(--cp-input-soft-bg-hover)] md:w-40"
|
||||
/>
|
||||
|
||||
<ProviderFilterSegmented
|
||||
v-model="provider"
|
||||
class="w-31 shrink-0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex shrink-0 self-end items-center justify-end gap-2 md:ml-auto">
|
||||
<BaseButton
|
||||
v-if="selectedCount > 0"
|
||||
variant="danger"
|
||||
:disabled="batchDeleting"
|
||||
@click="emit('deleteSelected')"
|
||||
>
|
||||
<Trash2 class="size-4" />
|
||||
删除选中 ({{ selectedCount }})
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
v-if="selectedCount > 0"
|
||||
variant="default"
|
||||
:loading="exportingAccounts"
|
||||
@click="emit('exportSelected')"
|
||||
>
|
||||
<Download class="size-4" />
|
||||
导出选中 ({{ selectedCount }})
|
||||
</BaseButton>
|
||||
<BaseButton variant="primary" @click="emit('create')">
|
||||
<Upload class="size-4" />
|
||||
导入账号
|
||||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -6,6 +6,7 @@ import { computed, onBeforeUnmount, ref, shallowRef, watch } from 'vue'
|
||||
import { getAccountModels, refreshAccountModels } from '@/api'
|
||||
import { API_BASE_URL } from '@/api/constants'
|
||||
import { toast } from '@/components/base/BaseToast'
|
||||
import { useIdSet } from '@/composables/useIdSet'
|
||||
import { errorMessage, withMinimumDuration } from '@/utils/async'
|
||||
import { formatDateTime, formatTime } from '@/utils/date'
|
||||
|
||||
@@ -49,7 +50,7 @@ export function useAccountConnectionTest(options: AccountConnectionTestOptions)
|
||||
const connectionTestStartedAt = shallowRef('')
|
||||
const connectionTestFinishedAt = shallowRef('')
|
||||
const connectionTestDurationMs = shallowRef<number | null>(null)
|
||||
const testingConnectionIds = ref<Set<string>>(new Set())
|
||||
const testingConnections = useIdSet<string>()
|
||||
const loadingConnectionTestModels = shallowRef(false)
|
||||
const refreshingConnectionTestModels = shallowRef(false)
|
||||
const connectionTestSelectedModel = shallowRef('')
|
||||
@@ -218,9 +219,7 @@ export function useAccountConnectionTest(options: AccountConnectionTestOptions)
|
||||
connectionTestRun = undefined
|
||||
closeConnectionTestEventSource()
|
||||
if (run) {
|
||||
const next = new Set(testingConnectionIds.value)
|
||||
next.delete(run.accountId)
|
||||
testingConnectionIds.value = next
|
||||
testingConnections.remove(run.accountId)
|
||||
run.resolve()
|
||||
}
|
||||
}
|
||||
@@ -350,7 +349,7 @@ export function useAccountConnectionTest(options: AccountConnectionTestOptions)
|
||||
connectionTestError.value = '请先选择测试模型'
|
||||
return
|
||||
}
|
||||
if (testingConnectionIds.value.has(account.id))
|
||||
if (testingConnections.has(account.id))
|
||||
return
|
||||
abortConnectionTest()
|
||||
connectionTestStatus.value = 'running'
|
||||
@@ -364,7 +363,7 @@ export function useAccountConnectionTest(options: AccountConnectionTestOptions)
|
||||
connectionTestStartedAt.value = formatDateTime()
|
||||
connectionTestFinishedAt.value = ''
|
||||
appendConnectionTestLog('准备发送测试请求', 'info')
|
||||
testingConnectionIds.value = new Set(testingConnectionIds.value).add(account.id)
|
||||
testingConnections.add(account.id)
|
||||
try {
|
||||
await withMinimumDuration(
|
||||
() =>
|
||||
@@ -436,7 +435,7 @@ export function useAccountConnectionTest(options: AccountConnectionTestOptions)
|
||||
connectionTestStartedAt,
|
||||
connectionTestFinishedAt,
|
||||
connectionTestDurationMs,
|
||||
testingConnectionIds,
|
||||
testingConnectionIds: testingConnections.ids,
|
||||
loadingConnectionTestModels,
|
||||
refreshingConnectionTestModels,
|
||||
connectionTestSelectedModel,
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { ChevronDown, Download, Search, Trash2, Upload } from '@lucide/vue'
|
||||
import { ChevronDown } from '@lucide/vue'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
import BaseCard from '@/components/base/BaseCard.vue'
|
||||
import BaseCheckbox from '@/components/base/BaseCheckbox.vue'
|
||||
import BaseConfirmModal from '@/components/base/BaseConfirmModal.vue'
|
||||
import BaseInput from '@/components/base/BaseInput.vue'
|
||||
import BasePageHeader from '@/components/base/BasePageHeader.vue'
|
||||
import BaseSelect from '@/components/base/BaseSelect.vue'
|
||||
import BaseTable from '@/components/base/BaseTable/index.vue'
|
||||
import ProviderFilterSegmented from '@/components/ProviderFilterSegmented.vue'
|
||||
import ProviderIconGroup from '@/components/ProviderIconGroup.vue'
|
||||
import AccountConnectionTestModal from './components/AccountConnectionTestModal.vue'
|
||||
import AccountCreateModal from './components/AccountCreateModal.vue'
|
||||
import AccountFilters from './components/AccountFilters.vue'
|
||||
import AccountIdentityCell from './components/AccountIdentityCell.vue'
|
||||
import AccountOverviewCards from './components/AccountOverviewCards.vue'
|
||||
import AccountPlanBadge from './components/AccountPlanBadge.vue'
|
||||
@@ -26,7 +23,7 @@ import { useAccountConnectionTest } from './composables/useAccountConnectionTest
|
||||
import { useAccountMutations } from './composables/useAccountMutations'
|
||||
import { useAccountsQuery } from './composables/useAccountsQuery'
|
||||
import { useAccountsTable } from './composables/useAccountsTable'
|
||||
import { accountColumns, accountStatusFilterOptions } from './constants'
|
||||
import { accountColumns } from './constants'
|
||||
|
||||
const selectedIds = ref<Set<string>>(new Set())
|
||||
const {
|
||||
@@ -137,60 +134,17 @@ const {
|
||||
body-class="flex min-h-0 flex-1 px-4 pb-3 md:px-5"
|
||||
>
|
||||
<template #header>
|
||||
<div
|
||||
class="flex w-full flex-col gap-3 md:flex-row md:flex-wrap md:items-center"
|
||||
role="group"
|
||||
aria-label="账号筛选与操作"
|
||||
>
|
||||
<div class="flex min-w-0 items-center gap-2 md:flex-none md:gap-3">
|
||||
<BaseInput
|
||||
v-model="searchQuery"
|
||||
placeholder="搜索邮箱或 ID..."
|
||||
class="min-w-0 flex-1 [--cp-input-current-bg:var(--cp-input-soft-bg)] [--cp-input-current-bg-hover:var(--cp-input-soft-bg-hover)] md:w-80 md:flex-none"
|
||||
>
|
||||
<template #prefix>
|
||||
<Search class="size-4.5 text-(--cp-text-tertiary)" />
|
||||
</template>
|
||||
</BaseInput>
|
||||
|
||||
<BaseSelect
|
||||
v-model="statusQuery"
|
||||
:options="accountStatusFilterOptions"
|
||||
aria-label="按账号状态筛选"
|
||||
class="w-34 shrink-0 [--cp-input-current-bg:var(--cp-input-soft-bg)] [--cp-input-current-bg-hover:var(--cp-input-soft-bg-hover)] md:w-40"
|
||||
/>
|
||||
|
||||
<ProviderFilterSegmented
|
||||
v-model="providerQuery"
|
||||
class="w-31 shrink-0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex shrink-0 self-end items-center justify-end gap-2 md:ml-auto">
|
||||
<BaseButton
|
||||
v-if="selectedIds.size > 0"
|
||||
variant="danger"
|
||||
:disabled="batchDeleting"
|
||||
@click="showDeleteModal = true"
|
||||
>
|
||||
<Trash2 class="size-4" />
|
||||
删除选中 ({{ selectedIds.size }})
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
v-if="selectedIds.size > 0"
|
||||
variant="default"
|
||||
:loading="exportingAccounts"
|
||||
@click="handleExportAccounts"
|
||||
>
|
||||
<Download class="size-4" />
|
||||
导出选中 ({{ selectedIds.size }})
|
||||
</BaseButton>
|
||||
<BaseButton variant="primary" @click="openCreateAccount">
|
||||
<Upload class="size-4" />
|
||||
导入账号
|
||||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
<AccountFilters
|
||||
v-model:search="searchQuery"
|
||||
v-model:status="statusQuery"
|
||||
v-model:provider="providerQuery"
|
||||
:selected-count="selectedIds.size"
|
||||
:batch-deleting="batchDeleting"
|
||||
:exporting-accounts="exportingAccounts"
|
||||
@delete-selected="showDeleteModal = true"
|
||||
@export-selected="handleExportAccounts"
|
||||
@create="openCreateAccount"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import type { Ref, ShallowRef } from 'vue'
|
||||
import type { getApiKeys } from '@/api'
|
||||
import { computed, shallowRef, watch } from 'vue'
|
||||
|
||||
import { API_BASE_URL } from '@/api/constants'
|
||||
import { toast } from '@/components/base/BaseToast'
|
||||
import { errorMessage } from '@/utils/async'
|
||||
import { buildCodexCcSwitchImportDeeplink } from '../utils/ccswitchImport'
|
||||
|
||||
type ApiKeyRow = Awaited<ReturnType<typeof getApiKeys>>['items'][number]
|
||||
|
||||
// 密钥使用与 CCSwitch 导入编排:服务根地址推导、deeplink 跳转、
|
||||
// “使用密钥”弹窗的明文补全与打开。
|
||||
export function useApiKeyUse(options: {
|
||||
createdKey: Readonly<Ref<string>>
|
||||
createdKeyName: Readonly<Ref<string>>
|
||||
createdKeyProviderKind: Readonly<Ref<string>>
|
||||
revealPlaintextKey: (apiKey: ApiKeyRow) => Promise<string>
|
||||
}) {
|
||||
const showUseKeyModal = shallowRef(false)
|
||||
const selectedUseKey: ShallowRef<ApiKeyRow | null> = shallowRef(null)
|
||||
|
||||
const serviceRootUrl = computed(() => resolveServiceRootUrl())
|
||||
const openAiBaseUrl = computed(() => `${serviceRootUrl.value}/v1`)
|
||||
|
||||
function resolveServiceRootUrl() {
|
||||
const normalizedApiBase = API_BASE_URL.trim().replace(/\/+$/, '')
|
||||
|
||||
if (/^https?:\/\//i.test(normalizedApiBase)) {
|
||||
return normalizedApiBase
|
||||
}
|
||||
|
||||
if (typeof window === 'undefined') {
|
||||
return normalizedApiBase
|
||||
}
|
||||
|
||||
const origin = window.location.origin.replace(/\/+$/, '')
|
||||
if (!normalizedApiBase) {
|
||||
return origin
|
||||
}
|
||||
|
||||
return `${origin}${normalizedApiBase.startsWith('/') ? normalizedApiBase : `/${normalizedApiBase}`}`
|
||||
}
|
||||
|
||||
function importCreatedKeyToCcs() {
|
||||
if (!options.createdKey.value)
|
||||
return
|
||||
|
||||
window.location.href = buildCodexCcSwitchImportDeeplink({
|
||||
apiKey: options.createdKey.value,
|
||||
baseUrl: openAiBaseUrl.value,
|
||||
providerName: options.createdKeyName.value || 'codex-proxy-rs',
|
||||
providerKind: options.createdKeyProviderKind.value,
|
||||
})
|
||||
}
|
||||
|
||||
async function openUseKeyModal(apiKey: ApiKeyRow) {
|
||||
try {
|
||||
const key = await options.revealPlaintextKey(apiKey)
|
||||
selectedUseKey.value = { ...apiKey, key }
|
||||
showUseKeyModal.value = true
|
||||
}
|
||||
catch (error: unknown) {
|
||||
toast.error(errorMessage(error, '读取完整密钥失败'))
|
||||
}
|
||||
}
|
||||
|
||||
async function importToCcs(apiKey: ApiKeyRow) {
|
||||
try {
|
||||
const key = await options.revealPlaintextKey(apiKey)
|
||||
window.location.href = buildCodexCcSwitchImportDeeplink({
|
||||
apiKey: key,
|
||||
baseUrl: openAiBaseUrl.value,
|
||||
providerName: apiKey.name || apiKey.prefix || 'codex-proxy-rs',
|
||||
providerKind: apiKey.providerKind,
|
||||
})
|
||||
}
|
||||
catch (error: unknown) {
|
||||
toast.error(errorMessage(error, '读取完整密钥失败'))
|
||||
}
|
||||
}
|
||||
|
||||
watch(showUseKeyModal, (open) => {
|
||||
if (!open)
|
||||
selectedUseKey.value = null
|
||||
})
|
||||
|
||||
return {
|
||||
showUseKeyModal,
|
||||
selectedUseKey,
|
||||
serviceRootUrl,
|
||||
openAiBaseUrl,
|
||||
importCreatedKeyToCcs,
|
||||
openUseKeyModal,
|
||||
importToCcs,
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, shallowRef, watch } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { API_BASE_URL } from '@/api/constants'
|
||||
import BaseCard from '@/components/base/BaseCard.vue'
|
||||
import BaseCheckbox from '@/components/base/BaseCheckbox.vue'
|
||||
import BaseConfirmModal from '@/components/base/BaseConfirmModal.vue'
|
||||
import BasePageHeader from '@/components/base/BasePageHeader.vue'
|
||||
import BaseTable from '@/components/base/BaseTable/index.vue'
|
||||
import { toast } from '@/components/base/BaseToast'
|
||||
import ProviderIconGroup from '@/components/ProviderIconGroup.vue'
|
||||
import { usePageSelection } from '@/composables/usePageSelection'
|
||||
import { errorMessage } from '@/utils/async'
|
||||
import ApiKeyActions from './components/ApiKeyActions.vue'
|
||||
import ApiKeyCreateModal from './components/ApiKeyCreateModal.vue'
|
||||
import ApiKeyFilters from './components/ApiKeyFilters.vue'
|
||||
@@ -20,11 +17,10 @@ import ApiKeyStatusBadge from './components/ApiKeyStatusBadge.vue'
|
||||
import ApiKeyUseModal from './components/ApiKeyUseModal.vue'
|
||||
import { useApiKeyMutations } from './composables/useApiKeyMutations'
|
||||
import { useApiKeysQuery } from './composables/useApiKeysQuery'
|
||||
import { useApiKeyUse } from './composables/useApiKeyUse'
|
||||
import { apiKeyColumns } from './constants'
|
||||
import { buildCodexCcSwitchImportDeeplink } from './utils/ccswitchImport'
|
||||
|
||||
const selectedIds = ref<Set<string>>(new Set())
|
||||
const showUseKeyModal = shallowRef(false)
|
||||
|
||||
const {
|
||||
loading,
|
||||
@@ -63,76 +59,24 @@ const {
|
||||
revealPlaintextKey,
|
||||
copyApiKey,
|
||||
} = useApiKeyMutations({ selectedIds, configRevision, reload: loadApiKeys })
|
||||
const selectedUseKey = shallowRef<(typeof apiKeys.value)[number] | null>(null)
|
||||
|
||||
const { allSelected, indeterminate, selectedRowKeys, toggleSelection, toggleAll } = usePageSelection(
|
||||
apiKeys,
|
||||
selectedIds,
|
||||
)
|
||||
|
||||
const serviceRootUrl = computed(() => resolveServiceRootUrl())
|
||||
const openAiBaseUrl = computed(() => `${serviceRootUrl.value}/v1`)
|
||||
|
||||
function resolveServiceRootUrl() {
|
||||
const normalizedApiBase = API_BASE_URL.trim().replace(/\/+$/, '')
|
||||
|
||||
if (/^https?:\/\//i.test(normalizedApiBase)) {
|
||||
return normalizedApiBase
|
||||
}
|
||||
|
||||
if (typeof window === 'undefined') {
|
||||
return normalizedApiBase
|
||||
}
|
||||
|
||||
const origin = window.location.origin.replace(/\/+$/, '')
|
||||
if (!normalizedApiBase) {
|
||||
return origin
|
||||
}
|
||||
|
||||
return `${origin}${normalizedApiBase.startsWith('/') ? normalizedApiBase : `/${normalizedApiBase}`}`
|
||||
}
|
||||
|
||||
function importCreatedKeyToCcs() {
|
||||
if (!createdKey.value)
|
||||
return
|
||||
|
||||
window.location.href = buildCodexCcSwitchImportDeeplink({
|
||||
apiKey: createdKey.value,
|
||||
baseUrl: openAiBaseUrl.value,
|
||||
providerName: createdKeyName.value || 'codex-proxy-rs',
|
||||
providerKind: createdKeyProviderKind.value,
|
||||
})
|
||||
}
|
||||
|
||||
async function openUseKeyModal(apiKey: (typeof apiKeys.value)[number]) {
|
||||
try {
|
||||
const key = await revealPlaintextKey(apiKey)
|
||||
selectedUseKey.value = { ...apiKey, key }
|
||||
showUseKeyModal.value = true
|
||||
}
|
||||
catch (error: unknown) {
|
||||
toast.error(errorMessage(error, '读取完整密钥失败'))
|
||||
}
|
||||
}
|
||||
|
||||
async function importToCcs(apiKey: (typeof apiKeys.value)[number]) {
|
||||
try {
|
||||
const key = await revealPlaintextKey(apiKey)
|
||||
window.location.href = buildCodexCcSwitchImportDeeplink({
|
||||
apiKey: key,
|
||||
baseUrl: openAiBaseUrl.value,
|
||||
providerName: apiKey.name || apiKey.prefix || 'codex-proxy-rs',
|
||||
providerKind: apiKey.providerKind,
|
||||
})
|
||||
}
|
||||
catch (error: unknown) {
|
||||
toast.error(errorMessage(error, '读取完整密钥失败'))
|
||||
}
|
||||
}
|
||||
|
||||
watch(showUseKeyModal, (open) => {
|
||||
if (!open)
|
||||
selectedUseKey.value = null
|
||||
const {
|
||||
showUseKeyModal,
|
||||
selectedUseKey,
|
||||
openAiBaseUrl,
|
||||
importCreatedKeyToCcs,
|
||||
openUseKeyModal,
|
||||
importToCcs,
|
||||
} = useApiKeyUse({
|
||||
createdKey,
|
||||
createdKeyName,
|
||||
createdKeyProviderKind,
|
||||
revealPlaintextKey,
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -6,17 +6,20 @@ import {
|
||||
regenerateAdminApiKey,
|
||||
} from '@/api'
|
||||
import { toast } from '@/components/base/BaseToast'
|
||||
import { useAsyncAction } from '@/composables/useAsyncAction'
|
||||
import { useCopyText } from '@/composables/useCopyText'
|
||||
import { errorMessage } from '@/utils/async'
|
||||
|
||||
export function useAdminApiKey() {
|
||||
const loading = shallowRef(true)
|
||||
const regenerating = shallowRef(false)
|
||||
const deleting = shallowRef(false)
|
||||
const showDeleteModal = shallowRef(false)
|
||||
const generatedKey = shallowRef('')
|
||||
const status = reactive({ exists: false })
|
||||
const copyText = useCopyText()
|
||||
const regenerateAction = useAsyncAction()
|
||||
const removeAction = useAsyncAction()
|
||||
const regenerating = regenerateAction.loading
|
||||
const deleting = removeAction.loading
|
||||
|
||||
async function loadStatus() {
|
||||
try {
|
||||
@@ -35,39 +38,31 @@ export function useAdminApiKey() {
|
||||
if (regenerating.value || deleting.value)
|
||||
return
|
||||
|
||||
try {
|
||||
regenerating.value = true
|
||||
const wasEnabled = status.exists
|
||||
generatedKey.value = (await regenerateAdminApiKey()).key
|
||||
status.exists = true
|
||||
toast.success(wasEnabled ? '管理员 API Key 已更新' : '管理员 API Key 已生成')
|
||||
}
|
||||
catch (error: unknown) {
|
||||
toast.error(errorMessage(error, '生成失败'))
|
||||
}
|
||||
finally {
|
||||
regenerating.value = false
|
||||
}
|
||||
await regenerateAction.run(
|
||||
async () => {
|
||||
const wasEnabled = status.exists
|
||||
generatedKey.value = (await regenerateAdminApiKey()).key
|
||||
status.exists = true
|
||||
toast.success(wasEnabled ? '管理员 API Key 已更新' : '管理员 API Key 已生成')
|
||||
},
|
||||
{ errorText: false, onError: error => toast.error(errorMessage(error, '生成失败')) },
|
||||
)
|
||||
}
|
||||
|
||||
async function remove() {
|
||||
if (deleting.value || regenerating.value)
|
||||
return
|
||||
|
||||
try {
|
||||
deleting.value = true
|
||||
await deleteAdminApiKey()
|
||||
status.exists = false
|
||||
generatedKey.value = ''
|
||||
showDeleteModal.value = false
|
||||
toast.success('管理员 API Key 已删除')
|
||||
}
|
||||
catch (error: unknown) {
|
||||
toast.error(errorMessage(error, '删除失败'))
|
||||
}
|
||||
finally {
|
||||
deleting.value = false
|
||||
}
|
||||
await removeAction.run(
|
||||
async () => {
|
||||
await deleteAdminApiKey()
|
||||
status.exists = false
|
||||
generatedKey.value = ''
|
||||
showDeleteModal.value = false
|
||||
toast.success('管理员 API Key 已删除')
|
||||
},
|
||||
{ errorText: false, onError: error => toast.error(errorMessage(error, '删除失败')) },
|
||||
)
|
||||
}
|
||||
|
||||
async function copyGeneratedKey() {
|
||||
|
||||
Reference in New Issue
Block a user