mirror of
https://github.com/fawney19/Aether.git
synced 2026-01-02 15:52:26 +08:00
feat: 引入统一的端点检查器以重构适配器并改进错误处理和用量统计。
This commit is contained in:
@@ -58,3 +58,16 @@ export async function deleteProvider(providerId: string): Promise<{ message: str
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试模型连接性
|
||||
*/
|
||||
export async function testModel(data: {
|
||||
provider_id: string
|
||||
model_name: string
|
||||
api_key_id?: string
|
||||
message?: string
|
||||
}): Promise<any> {
|
||||
const response = await client.post('/api/admin/provider-query/test-model', data)
|
||||
return response.data
|
||||
}
|
||||
|
||||
|
||||
@@ -163,7 +163,9 @@ const contentZIndex = computed(() => (props.zIndex || 60) + 10)
|
||||
useEscapeKey(() => {
|
||||
if (isOpen.value) {
|
||||
handleClose()
|
||||
return true // 阻止其他监听器(如父级抽屉的 ESC 监听器)
|
||||
}
|
||||
return false
|
||||
}, {
|
||||
disableOnInput: true,
|
||||
once: false
|
||||
|
||||
@@ -47,11 +47,11 @@ export function useConfirm() {
|
||||
/**
|
||||
* 便捷方法:危险操作确认(红色主题)
|
||||
*/
|
||||
const confirmDanger = (message: string, title?: string): Promise<boolean> => {
|
||||
const confirmDanger = (message: string, title?: string, confirmText?: string): Promise<boolean> => {
|
||||
return confirm({
|
||||
message,
|
||||
title: title || '危险操作',
|
||||
confirmText: '删除',
|
||||
confirmText: confirmText || '删除',
|
||||
variant: 'danger'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,11 +4,11 @@ import { onMounted, onUnmounted, ref } from 'vue'
|
||||
* ESC 键监听 Composable(简化版本,直接使用独立监听器)
|
||||
* 用于按 ESC 键关闭弹窗或其他可关闭的组件
|
||||
*
|
||||
* @param callback - 按 ESC 键时执行的回调函数
|
||||
* @param callback - 按 ESC 键时执行的回调函数,返回 true 表示已处理事件,阻止其他监听器执行
|
||||
* @param options - 配置选项
|
||||
*/
|
||||
export function useEscapeKey(
|
||||
callback: () => void,
|
||||
callback: () => void | boolean,
|
||||
options: {
|
||||
/** 是否在输入框获得焦点时禁用 ESC 键,默认 true */
|
||||
disableOnInput?: boolean
|
||||
@@ -42,8 +42,11 @@ export function useEscapeKey(
|
||||
if (isInputElement) return
|
||||
}
|
||||
|
||||
// 执行回调
|
||||
callback()
|
||||
// 执行回调,如果返回 true 则阻止其他监听器
|
||||
const handled = callback()
|
||||
if (handled === true) {
|
||||
event.stopImmediatePropagation()
|
||||
}
|
||||
|
||||
// 移除当前元素的焦点,避免残留样式
|
||||
if (document.activeElement instanceof HTMLElement) {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
v-model:open="modelSelectOpen"
|
||||
:model-value="formData.modelId"
|
||||
:disabled="!!editingGroup"
|
||||
@update:model-value="formData.modelId = $event"
|
||||
@update:model-value="handleModelChange"
|
||||
>
|
||||
<SelectTrigger class="h-9">
|
||||
<SelectValue placeholder="请选择模型" />
|
||||
@@ -518,6 +518,15 @@ function initForm() {
|
||||
upstreamModels.value = []
|
||||
}
|
||||
}
|
||||
|
||||
// 处理模型选择变更
|
||||
function handleModelChange(value: string) {
|
||||
formData.value.modelId = value
|
||||
const selectedModel = props.models.find(m => m.id === value)
|
||||
if (selectedModel) {
|
||||
upstreamModelSearch.value = selectedModel.provider_model_name
|
||||
}
|
||||
}
|
||||
|
||||
// 切换 API 格式
|
||||
function toggleApiFormat(format: string) {
|
||||
|
||||
@@ -531,6 +531,7 @@
|
||||
<!-- 模型名称映射 -->
|
||||
<ModelAliasesTab
|
||||
v-if="provider"
|
||||
ref="modelAliasesTabRef"
|
||||
:key="`aliases-${provider.id}`"
|
||||
:provider="provider"
|
||||
@refresh="handleRelatedDataRefresh"
|
||||
@@ -735,6 +736,9 @@ const deleteModelConfirmOpen = ref(false)
|
||||
const modelToDelete = ref<Model | null>(null)
|
||||
const batchAssignDialogOpen = ref(false)
|
||||
|
||||
// ModelAliasesTab 组件引用
|
||||
const modelAliasesTabRef = ref<InstanceType<typeof ModelAliasesTab> | null>(null)
|
||||
|
||||
// 拖动排序相关状态
|
||||
const dragState = ref({
|
||||
isDragging: false,
|
||||
@@ -756,7 +760,9 @@ const hasBlockingDialogOpen = computed(() =>
|
||||
deleteKeyConfirmOpen.value ||
|
||||
modelFormDialogOpen.value ||
|
||||
deleteModelConfirmOpen.value ||
|
||||
batchAssignDialogOpen.value
|
||||
batchAssignDialogOpen.value ||
|
||||
// 检测 ModelAliasesTab 子组件的 Dialog 是否打开
|
||||
modelAliasesTabRef.value?.dialogOpen
|
||||
)
|
||||
|
||||
// 监听 providerId 变化
|
||||
|
||||
@@ -110,16 +110,30 @@
|
||||
<div
|
||||
v-for="mapping in group.aliases"
|
||||
:key="mapping.name"
|
||||
class="flex items-center gap-2 py-1"
|
||||
class="flex items-center justify-between gap-2 py-1"
|
||||
>
|
||||
<!-- 优先级标签 -->
|
||||
<span class="inline-flex items-center justify-center w-5 h-5 rounded bg-background border text-xs font-medium shrink-0">
|
||||
{{ mapping.priority }}
|
||||
</span>
|
||||
<!-- 映射名称 -->
|
||||
<span class="font-mono text-sm truncate">
|
||||
{{ mapping.name }}
|
||||
</span>
|
||||
<div class="flex items-center gap-2 flex-1 min-w-0">
|
||||
<!-- 优先级标签 -->
|
||||
<span class="inline-flex items-center justify-center w-5 h-5 rounded bg-background border text-xs font-medium shrink-0">
|
||||
{{ mapping.priority }}
|
||||
</span>
|
||||
<!-- 映射名称 -->
|
||||
<span class="font-mono text-sm truncate">
|
||||
{{ mapping.name }}
|
||||
</span>
|
||||
</div>
|
||||
<!-- 测试按钮 -->
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7 shrink-0"
|
||||
title="测试映射"
|
||||
:disabled="testingMapping === `${group.model.id}-${group.apiFormatsKey}-${mapping.name}`"
|
||||
@click="testMapping(group, mapping)"
|
||||
>
|
||||
<Loader2 v-if="testingMapping === `${group.model.id}-${group.apiFormatsKey}-${mapping.name}`" class="w-3 h-3 animate-spin" />
|
||||
<Play v-else class="w-3 h-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -166,13 +180,14 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { Tag, Plus, Edit, Trash2, ChevronRight } from 'lucide-vue-next'
|
||||
import { Tag, Plus, Edit, Trash2, ChevronRight, Loader2, Play } from 'lucide-vue-next'
|
||||
import { Card, Button, Badge } from '@/components/ui'
|
||||
import AlertDialog from '@/components/common/AlertDialog.vue'
|
||||
import ModelMappingDialog, { type AliasGroup } from '../ModelMappingDialog.vue'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import {
|
||||
getProviderModels,
|
||||
testModel,
|
||||
API_FORMAT_LABELS,
|
||||
type Model,
|
||||
type ProviderModelAlias
|
||||
@@ -196,6 +211,7 @@ const dialogOpen = ref(false)
|
||||
const deleteConfirmOpen = ref(false)
|
||||
const editingGroup = ref<AliasGroup | null>(null)
|
||||
const deletingGroup = ref<AliasGroup | null>(null)
|
||||
const testingMapping = ref<string | null>(null)
|
||||
|
||||
// 列表展开状态
|
||||
const expandedAliasGroups = ref<Set<string>>(new Set())
|
||||
@@ -337,6 +353,81 @@ async function onDialogSaved() {
|
||||
emit('refresh')
|
||||
}
|
||||
|
||||
// 测试模型映射
|
||||
async function testMapping(group: any, mapping: any) {
|
||||
const testingKey = `${group.model.id}-${group.apiFormatsKey}-${mapping.name}`
|
||||
testingMapping.value = testingKey
|
||||
|
||||
try {
|
||||
// 根据分组的 API 格式来确定应该使用的格式
|
||||
let apiFormat = null
|
||||
if (group.apiFormats.length === 1) {
|
||||
apiFormat = group.apiFormats[0]
|
||||
} else if (group.apiFormats.length === 0) {
|
||||
// 如果没有指定格式,但分组显示为"全部",则使用模型的默认格式
|
||||
apiFormat = group.model.effective_api_format || group.model.api_format
|
||||
}
|
||||
|
||||
console.log(`测试映射 ${mapping.name},使用 API Format: ${apiFormat}`)
|
||||
|
||||
const result = await testModel({
|
||||
provider_id: props.provider.id,
|
||||
model_name: mapping.name, // 使用映射名称进行测试
|
||||
message: "hello",
|
||||
api_format: apiFormat
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
showSuccess(`映射 "${mapping.name}" 测试成功`)
|
||||
|
||||
// 如果有响应内容,可以显示更多信息
|
||||
if (result.data?.response?.choices?.[0]?.message?.content) {
|
||||
const content = result.data.response.choices[0].message.content
|
||||
showSuccess(`测试成功,响应: ${content.substring(0, 100)}${content.length > 100 ? '...' : ''}`)
|
||||
} else if (result.data?.content_preview) {
|
||||
showSuccess(`流式测试成功,预览: ${result.data.content_preview}`)
|
||||
}
|
||||
} else {
|
||||
// 根据不同的错误类型显示更详细的信息
|
||||
let errorMsg = result.error || '测试失败'
|
||||
|
||||
// 检查HTTP状态码错误
|
||||
if (result.data?.response?.status_code) {
|
||||
const status = result.data.response.status_code
|
||||
if (status === 403) {
|
||||
errorMsg = '认证失败: API密钥无效或客户端类型不被允许'
|
||||
} else if (status === 401) {
|
||||
errorMsg = '认证失败: API密钥无效或已过期'
|
||||
} else if (status === 404) {
|
||||
errorMsg = '模型不存在: 请检查模型名称是否正确'
|
||||
} else if (status === 429) {
|
||||
errorMsg = '请求频率过高: 请稍后重试'
|
||||
} else if (status >= 500) {
|
||||
errorMsg = `服务器错误: HTTP ${status}`
|
||||
} else {
|
||||
errorMsg = `请求失败: HTTP ${status}`
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试从错误响应中提取更多信息
|
||||
if (result.data?.response?.error) {
|
||||
if (typeof result.data.response.error === 'string') {
|
||||
errorMsg = result.data.response.error
|
||||
} else if (result.data.response.error?.message) {
|
||||
errorMsg = result.data.response.error.message
|
||||
}
|
||||
}
|
||||
|
||||
showError(`映射测试失败: ${errorMsg}`)
|
||||
}
|
||||
} catch (err: any) {
|
||||
const errorMsg = err.response?.data?.detail || err.message || '测试请求失败'
|
||||
showError(`映射测试失败: ${errorMsg}`)
|
||||
} finally {
|
||||
testingMapping.value = null
|
||||
}
|
||||
}
|
||||
|
||||
// 监听 provider 变化
|
||||
watch(() => props.provider?.id, (newId) => {
|
||||
if (newId) {
|
||||
@@ -349,4 +440,9 @@ onMounted(() => {
|
||||
loadModels()
|
||||
}
|
||||
})
|
||||
|
||||
// 暴露给父组件,用于检测是否有弹窗打开
|
||||
defineExpose({
|
||||
dialogOpen: computed(() => dialogOpen.value || deleteConfirmOpen.value)
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -156,6 +156,17 @@
|
||||
</td>
|
||||
<td class="align-top px-4 py-3">
|
||||
<div class="flex justify-center gap-1.5">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
title="测试模型"
|
||||
:disabled="testingModelId === model.id"
|
||||
@click="testModelConnection(model)"
|
||||
>
|
||||
<Loader2 v-if="testingModelId === model.id" class="w-3.5 h-3.5 animate-spin" />
|
||||
<Play v-else class="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -209,11 +220,11 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { Box, Edit, Trash2, Layers, Eye, Wrench, Zap, Brain, Power, Copy, Image } from 'lucide-vue-next'
|
||||
import { Box, Edit, Trash2, Layers, Eye, Wrench, Zap, Brain, Power, Copy, Image, Loader2, Play } from 'lucide-vue-next'
|
||||
import Card from '@/components/ui/card.vue'
|
||||
import Button from '@/components/ui/button.vue'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { getProviderModels, type Model } from '@/api/endpoints'
|
||||
import { getProviderModels, testModel, type Model } from '@/api/endpoints'
|
||||
import { updateModel } from '@/api/endpoints/models'
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -232,6 +243,7 @@ const { error: showError, success: showSuccess } = useToast()
|
||||
const loading = ref(false)
|
||||
const models = ref<Model[]>([])
|
||||
const togglingModelId = ref<string | null>(null)
|
||||
const testingModelId = ref<string | null>(null)
|
||||
|
||||
// 按名称排序的模型列表
|
||||
const sortedModels = computed(() => {
|
||||
@@ -380,6 +392,69 @@ async function toggleModelActive(model: Model) {
|
||||
}
|
||||
}
|
||||
|
||||
// 测试模型连接性
|
||||
async function testModelConnection(model: Model) {
|
||||
if (testingModelId.value) return
|
||||
|
||||
testingModelId.value = model.id
|
||||
try {
|
||||
const result = await testModel({
|
||||
provider_id: props.provider.id,
|
||||
model_name: model.provider_model_name,
|
||||
message: "hello"
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
showSuccess(`模型 "${model.provider_model_name}" 测试成功`)
|
||||
|
||||
// 如果有响应内容,可以显示更多信息
|
||||
if (result.data?.response?.choices?.[0]?.message?.content) {
|
||||
const content = result.data.response.choices[0].message.content
|
||||
showSuccess(`测试成功,响应: ${content.substring(0, 100)}${content.length > 100 ? '...' : ''}`)
|
||||
} else if (result.data?.content_preview) {
|
||||
showSuccess(`流式测试成功,预览: ${result.data.content_preview}`)
|
||||
}
|
||||
} else {
|
||||
// 根据不同的错误类型显示更详细的信息
|
||||
let errorMsg = result.error || '测试失败'
|
||||
|
||||
// 检查HTTP状态码错误
|
||||
if (result.data?.response?.status_code) {
|
||||
const status = result.data.response.status_code
|
||||
if (status === 403) {
|
||||
errorMsg = '认证失败: API密钥无效或客户端类型不被允许'
|
||||
} else if (status === 401) {
|
||||
errorMsg = '认证失败: API密钥无效或已过期'
|
||||
} else if (status === 404) {
|
||||
errorMsg = '模型不存在: 请检查模型名称是否正确'
|
||||
} else if (status === 429) {
|
||||
errorMsg = '请求频率过高: 请稍后重试'
|
||||
} else if (status >= 500) {
|
||||
errorMsg = `服务器错误: HTTP ${status}`
|
||||
} else {
|
||||
errorMsg = `请求失败: HTTP ${status}`
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试从错误响应中提取更多信息
|
||||
if (result.data?.response?.error) {
|
||||
if (typeof result.data.response.error === 'string') {
|
||||
errorMsg = result.data.response.error
|
||||
} else if (result.data.response.error?.message) {
|
||||
errorMsg = result.data.response.error.message
|
||||
}
|
||||
}
|
||||
|
||||
showError(`模型测试失败: ${errorMsg}`)
|
||||
}
|
||||
} catch (err: any) {
|
||||
const errorMsg = err.response?.data?.detail || err.message || '测试请求失败'
|
||||
showError(`模型测试失败: ${errorMsg}`)
|
||||
} finally {
|
||||
testingModelId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadModels()
|
||||
})
|
||||
|
||||
@@ -14,7 +14,7 @@ export const useUsersStore = defineStore('users', () => {
|
||||
try {
|
||||
users.value = await usersApi.getAllUsers()
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.detail || '获取用户列表失败'
|
||||
error.value = err.response?.data?.error?.message || err.response?.data?.detail || '获取用户列表失败'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -29,7 +29,7 @@ export const useUsersStore = defineStore('users', () => {
|
||||
users.value.push(newUser)
|
||||
return newUser
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.detail || '创建用户失败'
|
||||
error.value = err.response?.data?.error?.message || err.response?.data?.detail || '创建用户失败'
|
||||
throw err
|
||||
} finally {
|
||||
loading.value = false
|
||||
@@ -52,7 +52,7 @@ export const useUsersStore = defineStore('users', () => {
|
||||
}
|
||||
return updatedUser
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.detail || '更新用户失败'
|
||||
error.value = err.response?.data?.error?.message || err.response?.data?.detail || '更新用户失败'
|
||||
throw err
|
||||
} finally {
|
||||
loading.value = false
|
||||
@@ -67,7 +67,7 @@ export const useUsersStore = defineStore('users', () => {
|
||||
await usersApi.deleteUser(userId)
|
||||
users.value = users.value.filter(u => u.id !== userId)
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.detail || '删除用户失败'
|
||||
error.value = err.response?.data?.error?.message || err.response?.data?.detail || '删除用户失败'
|
||||
throw err
|
||||
} finally {
|
||||
loading.value = false
|
||||
@@ -78,7 +78,7 @@ export const useUsersStore = defineStore('users', () => {
|
||||
try {
|
||||
return await usersApi.getUserApiKeys(userId)
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.detail || '获取 API Keys 失败'
|
||||
error.value = err.response?.data?.error?.message || err.response?.data?.detail || '获取 API Keys 失败'
|
||||
throw err
|
||||
}
|
||||
}
|
||||
@@ -87,7 +87,7 @@ export const useUsersStore = defineStore('users', () => {
|
||||
try {
|
||||
return await usersApi.createApiKey(userId, name)
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.detail || '创建 API Key 失败'
|
||||
error.value = err.response?.data?.error?.message || err.response?.data?.detail || '创建 API Key 失败'
|
||||
throw err
|
||||
}
|
||||
}
|
||||
@@ -96,7 +96,7 @@ export const useUsersStore = defineStore('users', () => {
|
||||
try {
|
||||
await usersApi.deleteApiKey(userId, keyId)
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.detail || '删除 API Key 失败'
|
||||
error.value = err.response?.data?.error?.message || err.response?.data?.detail || '删除 API Key 失败'
|
||||
throw err
|
||||
}
|
||||
}
|
||||
@@ -110,7 +110,7 @@ export const useUsersStore = defineStore('users', () => {
|
||||
// 刷新用户列表以获取最新数据
|
||||
await fetchUsers()
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.detail || '重置配额失败'
|
||||
error.value = err.response?.data?.error?.message || err.response?.data?.detail || '重置配额失败'
|
||||
throw err
|
||||
} finally {
|
||||
loading.value = false
|
||||
|
||||
@@ -723,9 +723,19 @@ async function handleDeleteProvider(provider: ProviderWithEndpointsSummary) {
|
||||
// 切换提供商状态
|
||||
async function toggleProviderStatus(provider: ProviderWithEndpointsSummary) {
|
||||
try {
|
||||
await updateProvider(provider.id, { is_active: !provider.is_active })
|
||||
provider.is_active = !provider.is_active
|
||||
showSuccess(provider.is_active ? '提供商已启用' : '提供商已停用')
|
||||
const newStatus = !provider.is_active
|
||||
await updateProvider(provider.id, { is_active: newStatus })
|
||||
|
||||
// 更新抽屉内部的 provider 对象
|
||||
provider.is_active = newStatus
|
||||
|
||||
// 同时更新主页面 providers 数组中的对象,实现无感更新
|
||||
const targetProvider = providers.value.find(p => p.id === provider.id)
|
||||
if (targetProvider) {
|
||||
targetProvider.is_active = newStatus
|
||||
}
|
||||
|
||||
showSuccess(newStatus ? '提供商已启用' : '提供商已停用')
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || '操作失败', '错误')
|
||||
}
|
||||
|
||||
@@ -875,7 +875,8 @@ async function toggleUserStatus(user: any) {
|
||||
const action = user.is_active ? '禁用' : '启用'
|
||||
const confirmed = await confirmDanger(
|
||||
`确定要${action}用户 ${user.username} 吗?`,
|
||||
`${action}用户`
|
||||
`${action}用户`,
|
||||
action
|
||||
)
|
||||
|
||||
if (!confirmed) return
|
||||
@@ -884,7 +885,7 @@ async function toggleUserStatus(user: any) {
|
||||
await usersStore.updateUser(user.id, { is_active: !user.is_active })
|
||||
success(`用户已${action}`)
|
||||
} catch (err: any) {
|
||||
error(err.response?.data?.detail || '未知错误', `${action}用户失败`)
|
||||
error(err.response?.data?.error?.message || err.response?.data?.detail || '未知错误', `${action}用户失败`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -955,7 +956,7 @@ async function handleUserFormSubmit(data: UserFormData & { password?: string })
|
||||
closeUserFormDialog()
|
||||
} catch (err: any) {
|
||||
const title = data.id ? '更新用户失败' : '创建用户失败'
|
||||
error(err.response?.data?.detail || '未知错误', title)
|
||||
error(err.response?.data?.error?.message || err.response?.data?.detail || '未知错误', title)
|
||||
} finally {
|
||||
userFormDialogRef.value?.setSaving(false)
|
||||
}
|
||||
@@ -989,7 +990,7 @@ async function createApiKey() {
|
||||
showNewApiKeyDialog.value = true
|
||||
await loadUserApiKeys(selectedUser.value.id)
|
||||
} catch (err: any) {
|
||||
error(err.response?.data?.detail || '未知错误', '创建 API Key 失败')
|
||||
error(err.response?.data?.error?.message || err.response?.data?.detail || '未知错误', '创建 API Key 失败')
|
||||
} finally {
|
||||
creatingApiKey.value = false
|
||||
}
|
||||
@@ -1026,7 +1027,7 @@ async function deleteApiKey(apiKey: any) {
|
||||
await loadUserApiKeys(selectedUser.value.id)
|
||||
success('API Key已删除')
|
||||
} catch (err: any) {
|
||||
error(err.response?.data?.detail || '未知错误', '删除 API Key 失败')
|
||||
error(err.response?.data?.error?.message || err.response?.data?.detail || '未知错误', '删除 API Key 失败')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1038,7 +1039,7 @@ async function copyFullKey(apiKey: any) {
|
||||
success('完整密钥已复制到剪贴板')
|
||||
} catch (err: any) {
|
||||
log.error('复制密钥失败:', err)
|
||||
error(err.response?.data?.detail || '未知错误', '复制密钥失败')
|
||||
error(err.response?.data?.error?.message || err.response?.data?.detail || '未知错误', '复制密钥失败')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1054,7 +1055,7 @@ async function resetQuota(user: any) {
|
||||
await usersStore.resetUserQuota(user.id)
|
||||
success('配额已重置')
|
||||
} catch (err: any) {
|
||||
error(err.response?.data?.detail || '未知错误', '重置配额失败')
|
||||
error(err.response?.data?.error?.message || err.response?.data?.detail || '未知错误', '重置配额失败')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1070,7 +1071,7 @@ async function deleteUser(user: any) {
|
||||
await usersStore.deleteUser(user.id)
|
||||
success('用户已删除')
|
||||
} catch (err: any) {
|
||||
error(err.response?.data?.detail || '未知错误', '删除用户失败')
|
||||
error(err.response?.data?.error?.message || err.response?.data?.detail || '未知错误', '删除用户失败')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -102,9 +102,9 @@
|
||||
<!-- Main Content -->
|
||||
<main class="relative z-10">
|
||||
<!-- Fixed Logo Container -->
|
||||
<div class="fixed inset-0 z-20 pointer-events-none flex items-center justify-center overflow-hidden">
|
||||
<div class="mt-4 fixed inset-0 z-20 pointer-events-none flex items-center justify-center overflow-hidden">
|
||||
<div
|
||||
class="transform-gpu logo-container"
|
||||
class="mt-16 transform-gpu logo-container"
|
||||
:class="[currentSection === SECTIONS.HOME ? 'home-section' : '', `logo-transition-${scrollDirection}`]"
|
||||
:style="fixedLogoStyle"
|
||||
>
|
||||
@@ -151,7 +151,7 @@
|
||||
class="min-h-screen snap-start flex items-center justify-center px-16 lg:px-20 py-20"
|
||||
>
|
||||
<div class="max-w-4xl mx-auto text-center">
|
||||
<div class="h-80 w-full mb-16" />
|
||||
<div class="h-80 w-full mb-16 mt-8" />
|
||||
<h1
|
||||
class="mb-6 text-5xl md:text-7xl font-bold text-[#191919] dark:text-white leading-tight transition-all duration-700"
|
||||
:style="getTitleStyle(SECTIONS.HOME)"
|
||||
@@ -166,7 +166,7 @@
|
||||
整合 Claude Code、Codex CLI、Gemini CLI 等多个 AI 编程助手
|
||||
</p>
|
||||
<button
|
||||
class="mt-16 transition-all duration-700 cursor-pointer hover:scale-110"
|
||||
class="mt-8 transition-all duration-700 cursor-pointer hover:scale-110"
|
||||
:style="getScrollIndicatorStyle(SECTIONS.HOME)"
|
||||
@click="scrollToSection(SECTIONS.CLAUDE)"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user