mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-24 19:52:30 +00:00
1
This commit is contained in:
@@ -3804,8 +3804,9 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted } from 'vue'
|
||||
import { showToast } from '@/utils/toast'
|
||||
import { apiClient } from '@/config/api'
|
||||
import { showToast } from '@/utils/tools'
|
||||
import * as httpApi from '@/utils/http_apis'
|
||||
import { getModels } from '@/utils/http_apis'
|
||||
import { useAccountsStore } from '@/stores/accounts'
|
||||
import { useConfirm } from '@/composables/useConfirm'
|
||||
import ProxyConfig from './ProxyConfig.vue'
|
||||
@@ -4110,20 +4111,20 @@ const allowedModels = ref([
|
||||
'claude-3-5-haiku-20241022'
|
||||
]) // 白名单模式下选中的模型列表
|
||||
|
||||
// 常用模型列表
|
||||
const commonModels = [
|
||||
{ value: 'claude-opus-4-5-20251101', label: 'Claude Opus 4.5', color: 'blue' },
|
||||
{ value: 'claude-sonnet-4-20250514', label: 'Claude Sonnet 4', color: 'blue' },
|
||||
{ value: 'claude-sonnet-4-5-20250929', label: 'Claude Sonnet 4.5', color: 'indigo' },
|
||||
{ value: 'claude-3-5-haiku-20241022', label: 'Claude 3.5 Haiku', color: 'green' },
|
||||
{ value: 'claude-haiku-4-5-20251001', label: 'Claude Haiku 4.5', color: 'emerald' },
|
||||
{ value: 'claude-opus-4-20250514', label: 'Claude Opus 4', color: 'purple' },
|
||||
{ value: 'claude-opus-4-1-20250805', label: 'Claude Opus 4.1', color: 'purple' },
|
||||
{ value: 'deepseek-chat', label: 'DeepSeek Chat', color: 'cyan' },
|
||||
{ value: 'Qwen', label: 'Qwen', color: 'orange' },
|
||||
{ value: 'Kimi', label: 'Kimi', color: 'pink' },
|
||||
{ value: 'GLM', label: 'GLM', color: 'teal' }
|
||||
]
|
||||
// 常用模型列表(从 API 获取)
|
||||
const commonModels = ref([])
|
||||
|
||||
// 加载模型列表
|
||||
const loadCommonModels = async () => {
|
||||
try {
|
||||
const result = await getModels()
|
||||
if (result.success && result.data?.all) {
|
||||
commonModels.value = result.data.all
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load models:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 模型映射表数据
|
||||
const modelMappings = ref([])
|
||||
@@ -4330,7 +4331,7 @@ const loadAccountUsage = async () => {
|
||||
if (!isEdit.value || !props.account?.id) return
|
||||
|
||||
try {
|
||||
const response = await apiClient.get(`/admin/claude-console-accounts/${props.account.id}/usage`)
|
||||
const response = await httpApi.get(`/admin/claude-console-accounts/${props.account.id}/usage`)
|
||||
if (response) {
|
||||
// 更新表单中的使用量数据
|
||||
form.value.dailyUsage = response.dailyUsage || 0
|
||||
@@ -5733,7 +5734,7 @@ const filteredGroups = computed(() => {
|
||||
const loadGroups = async () => {
|
||||
loadingGroups.value = true
|
||||
try {
|
||||
const response = await apiClient.get('/admin/account-groups')
|
||||
const response = await httpApi.get('/admin/account-groups')
|
||||
groups.value = response.data || []
|
||||
} catch (error) {
|
||||
showToast('加载分组列表失败', 'error')
|
||||
@@ -6186,7 +6187,7 @@ watch(
|
||||
// 否则查找账户所属的分组
|
||||
const checkPromises = groups.value.map(async (group) => {
|
||||
try {
|
||||
const response = await apiClient.get(`/admin/account-groups/${group.id}/members`)
|
||||
const response = await httpApi.get(`/admin/account-groups/${group.id}/members`)
|
||||
const members = response.data || []
|
||||
if (members.some((m) => m.id === newAccount.id)) {
|
||||
foundGroupIds.push(group.id)
|
||||
@@ -6214,7 +6215,7 @@ watch(
|
||||
// 获取统一 User-Agent 信息
|
||||
const fetchUnifiedUserAgent = async () => {
|
||||
try {
|
||||
const response = await apiClient.get('/admin/claude-code-version')
|
||||
const response = await httpApi.get('/admin/claude-code-version')
|
||||
if (response.success && response.userAgent) {
|
||||
unifiedUserAgent.value = response.userAgent
|
||||
} else {
|
||||
@@ -6230,7 +6231,7 @@ const fetchUnifiedUserAgent = async () => {
|
||||
const clearUnifiedCache = async () => {
|
||||
clearingCache.value = true
|
||||
try {
|
||||
const response = await apiClient.post('/admin/claude-code-version/clear')
|
||||
const response = await httpApi.post('/admin/claude-code-version/clear')
|
||||
if (response.success) {
|
||||
unifiedUserAgent.value = ''
|
||||
showToast('统一User-Agent缓存已清除', 'success')
|
||||
@@ -6336,6 +6337,9 @@ onMounted(() => {
|
||||
initModelMappings()
|
||||
}
|
||||
|
||||
// 加载模型列表
|
||||
loadCommonModels()
|
||||
|
||||
// 获取Claude Code统一User-Agent信息
|
||||
fetchUnifiedUserAgent()
|
||||
// 如果是编辑模式且是Claude Console账户,加载使用情况
|
||||
|
||||
@@ -220,8 +220,8 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { API_PREFIX } from '@/config/api'
|
||||
import { showToast } from '@/utils/toast'
|
||||
import { API_PREFIX } from '@/utils/http_apis'
|
||||
import { showToast } from '@/utils/tools'
|
||||
|
||||
const props = defineProps({
|
||||
show: {
|
||||
|
||||
@@ -70,7 +70,13 @@
|
||||
</div>
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-gray-500 dark:text-gray-400">测试模型</span>
|
||||
<span class="font-medium text-gray-700 dark:text-gray-300">{{ testModel }}</span>
|
||||
<select
|
||||
v-model="selectedModel"
|
||||
class="rounded-lg border border-gray-200 bg-white px-2 py-1 text-xs font-medium text-gray-700 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300"
|
||||
:disabled="testStatus === 'testing'"
|
||||
>
|
||||
<option v-for="m in availableModels" :key="m" :value="m">{{ m }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -177,7 +183,7 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, onUnmounted } from 'vue'
|
||||
import { API_PREFIX } from '@/config/api'
|
||||
import { API_PREFIX } from '@/utils/http_apis'
|
||||
|
||||
const props = defineProps({
|
||||
show: {
|
||||
@@ -199,36 +205,112 @@ const errorMessage = ref('')
|
||||
const testDuration = ref(0)
|
||||
const testStartTime = ref(null)
|
||||
const eventSource = ref(null)
|
||||
const selectedModel = ref('')
|
||||
|
||||
// 测试模型
|
||||
const testModel = ref('claude-sonnet-4-5-20250929')
|
||||
// 可用模型列表 - 根据账户类型
|
||||
const availableModels = computed(() => {
|
||||
if (!props.account) return []
|
||||
const platform = props.account.platform
|
||||
const modelLists = {
|
||||
claude: ['claude-sonnet-4-5-20250929', 'claude-sonnet-4-20250514', 'claude-3-5-haiku-20241022'],
|
||||
'claude-console': [
|
||||
'claude-sonnet-4-5-20250929',
|
||||
'claude-sonnet-4-20250514',
|
||||
'claude-3-5-haiku-20241022'
|
||||
],
|
||||
bedrock: [
|
||||
'claude-sonnet-4-5-20250929',
|
||||
'claude-sonnet-4-20250514',
|
||||
'claude-3-5-haiku-20241022'
|
||||
],
|
||||
gemini: ['gemini-2.5-flash', 'gemini-2.5-pro', 'gemini-2.0-flash'],
|
||||
'openai-responses': ['gpt-4o-mini', 'gpt-4o', 'o3-mini'],
|
||||
'azure-openai': [props.account.deploymentName || 'gpt-4o-mini'],
|
||||
droid: ['claude-sonnet-4-20250514', 'claude-3-5-haiku-20241022'],
|
||||
ccr: ['claude-sonnet-4-20250514', 'claude-3-5-haiku-20241022']
|
||||
}
|
||||
return modelLists[platform] || []
|
||||
})
|
||||
|
||||
// 默认测试模型
|
||||
const defaultModel = computed(() => {
|
||||
if (!props.account) return ''
|
||||
const platform = props.account.platform
|
||||
const models = {
|
||||
claude: 'claude-sonnet-4-5-20250929',
|
||||
'claude-console': 'claude-sonnet-4-5-20250929',
|
||||
bedrock: 'claude-sonnet-4-5-20250929',
|
||||
gemini: 'gemini-2.5-flash',
|
||||
'openai-responses': 'gpt-4o-mini',
|
||||
'azure-openai': props.account.deploymentName || 'gpt-4o-mini',
|
||||
droid: 'claude-sonnet-4-20250514',
|
||||
ccr: 'claude-sonnet-4-20250514'
|
||||
}
|
||||
return models[platform] || ''
|
||||
})
|
||||
|
||||
// 监听账户变化,重置选中的模型
|
||||
watch(
|
||||
() => props.account,
|
||||
() => {
|
||||
selectedModel.value = defaultModel.value
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// 是否使用 SSE 流式响应
|
||||
const useSSE = computed(() => {
|
||||
if (!props.account) return false
|
||||
return ['claude', 'claude-console'].includes(props.account.platform)
|
||||
})
|
||||
|
||||
// 计算属性
|
||||
const platformLabel = computed(() => {
|
||||
if (!props.account) return '未知'
|
||||
const platform = props.account.platform
|
||||
if (platform === 'claude') return 'Claude OAuth'
|
||||
if (platform === 'claude-console') return 'Claude Console'
|
||||
return platform
|
||||
const labels = {
|
||||
claude: 'Claude OAuth',
|
||||
'claude-console': 'Claude Console',
|
||||
bedrock: 'AWS Bedrock',
|
||||
gemini: 'Gemini',
|
||||
'openai-responses': 'OpenAI Responses',
|
||||
'azure-openai': 'Azure OpenAI',
|
||||
droid: 'Droid',
|
||||
ccr: 'CCR'
|
||||
}
|
||||
return labels[platform] || platform
|
||||
})
|
||||
|
||||
const platformIcon = computed(() => {
|
||||
if (!props.account) return 'fas fa-question'
|
||||
const platform = props.account.platform
|
||||
if (platform === 'claude' || platform === 'claude-console') return 'fas fa-brain'
|
||||
return 'fas fa-robot'
|
||||
const icons = {
|
||||
claude: 'fas fa-brain',
|
||||
'claude-console': 'fas fa-brain',
|
||||
bedrock: 'fab fa-aws',
|
||||
gemini: 'fas fa-gem',
|
||||
'openai-responses': 'fas fa-code',
|
||||
'azure-openai': 'fab fa-microsoft',
|
||||
droid: 'fas fa-robot',
|
||||
ccr: 'fas fa-key'
|
||||
}
|
||||
return icons[platform] || 'fas fa-robot'
|
||||
})
|
||||
|
||||
const platformBadgeClass = computed(() => {
|
||||
if (!props.account) return 'bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300'
|
||||
const platform = props.account.platform
|
||||
if (platform === 'claude') {
|
||||
return 'bg-indigo-100 text-indigo-700 dark:bg-indigo-500/20 dark:text-indigo-300'
|
||||
const classes = {
|
||||
claude: 'bg-indigo-100 text-indigo-700 dark:bg-indigo-500/20 dark:text-indigo-300',
|
||||
'claude-console': 'bg-purple-100 text-purple-700 dark:bg-purple-500/20 dark:text-purple-300',
|
||||
bedrock: 'bg-orange-100 text-orange-700 dark:bg-orange-500/20 dark:text-orange-300',
|
||||
gemini: 'bg-blue-100 text-blue-700 dark:bg-blue-500/20 dark:text-blue-300',
|
||||
'openai-responses': 'bg-green-100 text-green-700 dark:bg-green-500/20 dark:text-green-300',
|
||||
'azure-openai': 'bg-cyan-100 text-cyan-700 dark:bg-cyan-500/20 dark:text-cyan-300',
|
||||
droid: 'bg-pink-100 text-pink-700 dark:bg-pink-500/20 dark:text-pink-300',
|
||||
ccr: 'bg-amber-100 text-amber-700 dark:bg-amber-500/20 dark:text-amber-300'
|
||||
}
|
||||
if (platform === 'claude-console') {
|
||||
return 'bg-purple-100 text-purple-700 dark:bg-purple-500/20 dark:text-purple-300'
|
||||
}
|
||||
return 'bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300'
|
||||
return classes[platform] || 'bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300'
|
||||
})
|
||||
|
||||
const statusTitle = computed(() => {
|
||||
@@ -247,15 +329,16 @@ const statusTitle = computed(() => {
|
||||
})
|
||||
|
||||
const statusDescription = computed(() => {
|
||||
const apiName = platformLabel.value || 'API'
|
||||
switch (testStatus.value) {
|
||||
case 'idle':
|
||||
return '点击下方按钮开始测试账户连通性'
|
||||
case 'testing':
|
||||
return '正在发送测试请求并等待响应'
|
||||
case 'success':
|
||||
return '账户可以正常访问 Claude API'
|
||||
return `账户可以正常访问 ${apiName}`
|
||||
case 'error':
|
||||
return errorMessage.value || '无法连接到 Claude API'
|
||||
return errorMessage.value || `无法连接到 ${apiName}`
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
@@ -340,13 +423,17 @@ const statusTextClass = computed(() => {
|
||||
function getTestEndpoint() {
|
||||
if (!props.account) return ''
|
||||
const platform = props.account.platform
|
||||
if (platform === 'claude') {
|
||||
return `${API_PREFIX}/admin/claude-accounts/${props.account.id}/test`
|
||||
const endpoints = {
|
||||
claude: `${API_PREFIX}/admin/claude-accounts/${props.account.id}/test`,
|
||||
'claude-console': `${API_PREFIX}/admin/claude-console-accounts/${props.account.id}/test`,
|
||||
bedrock: `${API_PREFIX}/admin/bedrock-accounts/${props.account.id}/test`,
|
||||
gemini: `${API_PREFIX}/admin/gemini-accounts/${props.account.id}/test`,
|
||||
'openai-responses': `${API_PREFIX}/admin/openai-responses-accounts/${props.account.id}/test`,
|
||||
'azure-openai': `${API_PREFIX}/admin/azure-openai-accounts/${props.account.id}/test`,
|
||||
droid: `${API_PREFIX}/admin/droid-accounts/${props.account.id}/test`,
|
||||
ccr: `${API_PREFIX}/admin/ccr-accounts/${props.account.id}/test`
|
||||
}
|
||||
if (platform === 'claude-console') {
|
||||
return `${API_PREFIX}/admin/claude-console-accounts/${props.account.id}/test`
|
||||
}
|
||||
return ''
|
||||
return endpoints[platform] || ''
|
||||
}
|
||||
|
||||
async function startTest() {
|
||||
@@ -375,14 +462,14 @@ async function startTest() {
|
||||
// 获取认证token
|
||||
const authToken = localStorage.getItem('authToken')
|
||||
|
||||
// 使用fetch发送POST请求并处理SSE
|
||||
// 使用fetch发送POST请求
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: authToken ? `Bearer ${authToken}` : ''
|
||||
},
|
||||
body: JSON.stringify({ model: testModel.value })
|
||||
body: JSON.stringify({ model: selectedModel.value })
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -390,31 +477,46 @@ async function startTest() {
|
||||
throw new Error(errorData.message || `HTTP ${response.status}`)
|
||||
}
|
||||
|
||||
// 处理SSE流
|
||||
const reader = response.body.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
let streamDone = false
|
||||
// 根据账户类型处理响应
|
||||
if (useSSE.value) {
|
||||
// SSE 流式响应 (Claude/Console)
|
||||
const reader = response.body.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
let streamDone = false
|
||||
|
||||
while (!streamDone) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) {
|
||||
streamDone = true
|
||||
continue
|
||||
}
|
||||
while (!streamDone) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) {
|
||||
streamDone = true
|
||||
continue
|
||||
}
|
||||
|
||||
const chunk = decoder.decode(value)
|
||||
const lines = chunk.split('\n')
|
||||
const chunk = decoder.decode(value)
|
||||
const lines = chunk.split('\n')
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('data: ')) {
|
||||
try {
|
||||
const data = JSON.parse(line.substring(6))
|
||||
handleSSEEvent(data)
|
||||
} catch {
|
||||
// 忽略解析错误
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('data: ')) {
|
||||
try {
|
||||
const data = JSON.parse(line.substring(6))
|
||||
handleSSEEvent(data)
|
||||
} catch {
|
||||
// 忽略解析错误
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// JSON 响应 (其他平台)
|
||||
const data = await response.json()
|
||||
testDuration.value = Date.now() - testStartTime.value
|
||||
|
||||
if (data.success) {
|
||||
testStatus.value = 'success'
|
||||
responseText.value = data.data?.responseText || 'Test passed'
|
||||
} else {
|
||||
testStatus.value = 'error'
|
||||
errorMessage.value = data.message || 'Test failed'
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
testStatus.value = 'error'
|
||||
|
||||
@@ -381,13 +381,24 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ConfirmModal
|
||||
:cancel-text="confirmModalConfig.cancelText"
|
||||
:confirm-text="confirmModalConfig.confirmText"
|
||||
:message="confirmModalConfig.message"
|
||||
:show="showConfirmModal"
|
||||
:title="confirmModalConfig.title"
|
||||
:type="confirmModalConfig.type"
|
||||
@cancel="handleCancelModal"
|
||||
@confirm="handleConfirmModal"
|
||||
/>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { showToast } from '@/utils/toast'
|
||||
import { apiClient } from '@/config/api'
|
||||
import { showToast } from '@/utils/tools'
|
||||
import * as httpApi from '@/utils/http_apis'
|
||||
import ConfirmModal from '@/components/common/ConfirmModal.vue'
|
||||
|
||||
const props = defineProps({
|
||||
accountId: {
|
||||
@@ -417,6 +428,39 @@ const searchQuery = ref('')
|
||||
const searchMode = ref('fuzzy') // 'fuzzy' | 'exact'
|
||||
const batchDeleting = ref(false)
|
||||
|
||||
// ConfirmModal 状态
|
||||
const showConfirmModal = ref(false)
|
||||
const confirmModalConfig = ref({
|
||||
title: '',
|
||||
message: '',
|
||||
type: 'primary',
|
||||
confirmText: '确认',
|
||||
cancelText: '取消'
|
||||
})
|
||||
const confirmResolve = ref(null)
|
||||
|
||||
const showConfirm = (
|
||||
title,
|
||||
message,
|
||||
confirmText = '确认',
|
||||
cancelText = '取消',
|
||||
type = 'primary'
|
||||
) => {
|
||||
return new Promise((resolve) => {
|
||||
confirmModalConfig.value = { title, message, confirmText, cancelText, type }
|
||||
confirmResolve.value = resolve
|
||||
showConfirmModal.value = true
|
||||
})
|
||||
}
|
||||
const handleConfirmModal = () => {
|
||||
showConfirmModal.value = false
|
||||
confirmResolve.value?.(true)
|
||||
}
|
||||
const handleCancelModal = () => {
|
||||
showConfirmModal.value = false
|
||||
confirmResolve.value?.(false)
|
||||
}
|
||||
|
||||
// 掩码显示 API Key(提前声明供 computed 使用)
|
||||
const maskApiKey = (key) => {
|
||||
if (!key || key.length < 12) {
|
||||
@@ -474,7 +518,7 @@ const errorKeysCount = computed(() => {
|
||||
const loadApiKeys = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await apiClient.get(`/admin/droid-accounts/${props.accountId}`)
|
||||
const response = await httpApi.get(`/admin/droid-accounts/${props.accountId}`)
|
||||
const account = response.data
|
||||
|
||||
// 解析 apiKeys
|
||||
@@ -549,7 +593,15 @@ const loadApiKeys = async () => {
|
||||
|
||||
// 删除 API Key
|
||||
const deleteApiKey = async (apiKey) => {
|
||||
if (!confirm(`确定要删除 API Key "${maskApiKey(apiKey.key)}" 吗?`)) {
|
||||
if (
|
||||
!(await showConfirm(
|
||||
'删除 API Key',
|
||||
`确定要删除 API Key "${maskApiKey(apiKey.key)}" 吗?`,
|
||||
'删除',
|
||||
'取消',
|
||||
'danger'
|
||||
))
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -561,7 +613,7 @@ const deleteApiKey = async (apiKey) => {
|
||||
apiKeyUpdateMode: 'delete'
|
||||
}
|
||||
|
||||
await apiClient.put(`/admin/droid-accounts/${props.accountId}`, updateData)
|
||||
await httpApi.put(`/admin/droid-accounts/${props.accountId}`, updateData)
|
||||
|
||||
showToast('API Key 已删除', 'success')
|
||||
await loadApiKeys()
|
||||
@@ -577,9 +629,13 @@ const deleteApiKey = async (apiKey) => {
|
||||
// 重置 API Key 状态
|
||||
const resetApiKeyStatus = async (apiKey) => {
|
||||
if (
|
||||
!confirm(
|
||||
`确定要重置 API Key "${maskApiKey(apiKey.key)}" 的状态吗?这将清除错误信息并恢复为正常状态。`
|
||||
)
|
||||
!(await showConfirm(
|
||||
'重置状态',
|
||||
`确定要重置 API Key "${maskApiKey(apiKey.key)}" 的状态吗?这将清除错误信息并恢复为正常状态。`,
|
||||
'重置',
|
||||
'取消',
|
||||
'warning'
|
||||
))
|
||||
) {
|
||||
return
|
||||
}
|
||||
@@ -598,7 +654,7 @@ const resetApiKeyStatus = async (apiKey) => {
|
||||
apiKeyUpdateMode: 'update'
|
||||
}
|
||||
|
||||
await apiClient.put(`/admin/droid-accounts/${props.accountId}`, updateData)
|
||||
await httpApi.put(`/admin/droid-accounts/${props.accountId}`, updateData)
|
||||
|
||||
showToast('API Key 状态已重置', 'success')
|
||||
await loadApiKeys()
|
||||
@@ -619,7 +675,15 @@ const deleteAllErrorKeys = async () => {
|
||||
return
|
||||
}
|
||||
|
||||
if (!confirm(`确定要删除所有 ${errorKeys.length} 个异常状态的 API Key 吗?此操作不可恢复!`)) {
|
||||
if (
|
||||
!(await showConfirm(
|
||||
'删除异常 API Key',
|
||||
`确定要删除所有 ${errorKeys.length} 个异常状态的 API Key 吗?此操作不可恢复!`,
|
||||
'删除',
|
||||
'取消',
|
||||
'danger'
|
||||
))
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -631,7 +695,7 @@ const deleteAllErrorKeys = async () => {
|
||||
apiKeyUpdateMode: 'delete'
|
||||
}
|
||||
|
||||
await apiClient.put(`/admin/droid-accounts/${props.accountId}`, updateData)
|
||||
await httpApi.put(`/admin/droid-accounts/${props.accountId}`, updateData)
|
||||
|
||||
showToast(`成功删除 ${errorKeys.length} 个异常 API Key`, 'success')
|
||||
await loadApiKeys()
|
||||
@@ -652,15 +716,21 @@ const deleteAllKeys = async () => {
|
||||
}
|
||||
|
||||
if (
|
||||
!confirm(
|
||||
`确定要删除所有 ${apiKeys.value.length} 个 API Key 吗?此操作不可恢复!\n\n请再次确认:这将删除该账户下的所有 API Key。`
|
||||
)
|
||||
!(await showConfirm(
|
||||
'删除全部 API Key',
|
||||
`确定要删除所有 ${apiKeys.value.length} 个 API Key 吗?此操作不可恢复!\n\n请再次确认:这将删除该账户下的所有 API Key。`,
|
||||
'删除',
|
||||
'取消',
|
||||
'danger'
|
||||
))
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
// 二次确认
|
||||
if (!confirm('最后确认:真的要删除所有 API Key 吗?')) {
|
||||
if (
|
||||
!(await showConfirm('最后确认', '真的要删除所有 API Key 吗?', '确认删除', '取消', 'danger'))
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -672,7 +742,7 @@ const deleteAllKeys = async () => {
|
||||
apiKeyUpdateMode: 'delete'
|
||||
}
|
||||
|
||||
await apiClient.put(`/admin/droid-accounts/${props.accountId}`, updateData)
|
||||
await httpApi.put(`/admin/droid-accounts/${props.accountId}`, updateData)
|
||||
|
||||
showToast(`成功删除所有 ${keysToDelete.length} 个 API Key`, 'success')
|
||||
await loadApiKeys()
|
||||
|
||||
@@ -259,8 +259,8 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted } from 'vue'
|
||||
import { apiClient } from '@/config/api'
|
||||
import { showToast } from '@/utils/toast'
|
||||
import * as httpApi from '@/utils/http_apis'
|
||||
import { showToast } from '@/utils/tools'
|
||||
import ProxyConfig from '@/components/accounts/ProxyConfig.vue'
|
||||
|
||||
const props = defineProps({
|
||||
@@ -344,7 +344,7 @@ const submit = async () => {
|
||||
if (form.value.apiKey && form.value.apiKey.trim().length > 0) {
|
||||
updates.apiKey = form.value.apiKey
|
||||
}
|
||||
const res = await apiClient.put(`/admin/ccr-accounts/${props.account.id}`, updates)
|
||||
const res = await httpApi.put(`/admin/ccr-accounts/${props.account.id}`, updates)
|
||||
if (res.success) {
|
||||
// 不在这里显示 toast,由父组件统一处理
|
||||
emit('success')
|
||||
@@ -367,7 +367,7 @@ const submit = async () => {
|
||||
dailyQuota: Number(form.value.dailyQuota || 0),
|
||||
quotaResetTime: form.value.quotaResetTime || '00:00'
|
||||
}
|
||||
const res = await apiClient.post('/admin/ccr-accounts', payload)
|
||||
const res = await httpApi.post('/admin/ccr-accounts', payload)
|
||||
if (res.success) {
|
||||
// 不在这里显示 toast,由父组件统一处理
|
||||
emit('success')
|
||||
|
||||
@@ -21,72 +21,36 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 添加分组按钮 -->
|
||||
<div class="mb-6">
|
||||
<button class="btn btn-primary px-4 py-2" @click="showCreateForm = true">
|
||||
<i class="fas fa-plus mr-2" />
|
||||
创建新分组
|
||||
<!-- Tab 切换栏 -->
|
||||
<div class="mb-4 flex flex-wrap gap-2">
|
||||
<button
|
||||
v-for="tab in platformTabs"
|
||||
:key="tab.key"
|
||||
:class="[
|
||||
'rounded-lg px-3 py-1.5 text-sm font-medium transition-all',
|
||||
activeTab === tab.key
|
||||
? tab.key === 'claude'
|
||||
? 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-300'
|
||||
: tab.key === 'gemini'
|
||||
? 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300'
|
||||
: tab.key === 'droid'
|
||||
? 'bg-cyan-100 text-cyan-700 dark:bg-cyan-900/30 dark:text-cyan-300'
|
||||
: 'bg-gray-200 text-gray-700 dark:bg-gray-700 dark:text-gray-200'
|
||||
: 'bg-gray-100 text-gray-600 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700'
|
||||
]"
|
||||
@click="activeTab = tab.key"
|
||||
>
|
||||
{{ tab.label }}
|
||||
<span class="ml-1 text-xs opacity-70">({{ platformCounts[tab.key] }})</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 创建分组表单 -->
|
||||
<div v-if="showCreateForm" class="mb-6 rounded-lg border border-blue-200 bg-blue-50 p-4">
|
||||
<h4 class="mb-4 text-lg font-semibold text-gray-900">创建新分组</h4>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700">分组名称 *</label>
|
||||
<input
|
||||
v-model="createForm.name"
|
||||
class="form-input w-full"
|
||||
placeholder="输入分组名称"
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700">平台类型 *</label>
|
||||
<div class="flex gap-4">
|
||||
<label class="flex cursor-pointer items-center">
|
||||
<input v-model="createForm.platform" class="mr-2" type="radio" value="claude" />
|
||||
<span class="text-sm text-gray-700">Claude</span>
|
||||
</label>
|
||||
<label class="flex cursor-pointer items-center">
|
||||
<input v-model="createForm.platform" class="mr-2" type="radio" value="gemini" />
|
||||
<span class="text-sm text-gray-700">Gemini</span>
|
||||
</label>
|
||||
<label class="flex cursor-pointer items-center">
|
||||
<input v-model="createForm.platform" class="mr-2" type="radio" value="openai" />
|
||||
<span class="text-sm text-gray-700">OpenAI</span>
|
||||
</label>
|
||||
<label class="flex cursor-pointer items-center">
|
||||
<input v-model="createForm.platform" class="mr-2" type="radio" value="droid" />
|
||||
<span class="text-sm text-gray-700">Droid</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700">描述 (可选)</label>
|
||||
<textarea
|
||||
v-model="createForm.description"
|
||||
class="form-input w-full resize-none"
|
||||
placeholder="分组描述..."
|
||||
rows="2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3">
|
||||
<button
|
||||
class="btn btn-primary px-4 py-2"
|
||||
:disabled="!createForm.name || !createForm.platform || creating"
|
||||
@click="createGroup"
|
||||
>
|
||||
<div v-if="creating" class="loading-spinner mr-2" />
|
||||
{{ creating ? '创建中...' : '创建' }}
|
||||
</button>
|
||||
<button class="btn btn-secondary px-4 py-2" @click="cancelCreate">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 添加分组按钮 -->
|
||||
<div class="mb-6">
|
||||
<button class="btn btn-primary px-4 py-2" @click="openCreateForm">
|
||||
<i class="fas fa-plus mr-2" />
|
||||
创建新分组
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 分组列表 -->
|
||||
@@ -96,14 +60,17 @@
|
||||
<p class="text-gray-500">加载中...</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="groups.length === 0" class="rounded-lg bg-gray-50 py-8 text-center">
|
||||
<i class="fas fa-layer-group mb-4 text-4xl text-gray-300" />
|
||||
<p class="text-gray-500">暂无分组</p>
|
||||
<div
|
||||
v-else-if="filteredGroups.length === 0"
|
||||
class="rounded-lg bg-gray-50 py-8 text-center dark:bg-gray-800"
|
||||
>
|
||||
<i class="fas fa-layer-group mb-4 text-4xl text-gray-300 dark:text-gray-600" />
|
||||
<p class="text-gray-500 dark:text-gray-400">暂无分组</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div
|
||||
v-for="group in groups"
|
||||
v-for="group in filteredGroups"
|
||||
:key="group.id"
|
||||
class="rounded-lg border bg-white p-4 transition-shadow hover:shadow-md"
|
||||
>
|
||||
@@ -239,13 +206,106 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 创建分组模态框 -->
|
||||
<div
|
||||
v-if="showCreateForm"
|
||||
class="modal fixed inset-0 z-50 flex items-center justify-center p-3 sm:p-4"
|
||||
>
|
||||
<div class="modal-content w-full max-w-lg p-4 sm:p-6">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<h3 class="text-lg font-bold text-gray-900 dark:text-gray-100">创建新分组</h3>
|
||||
<button
|
||||
class="text-gray-400 transition-colors hover:text-gray-600 dark:hover:text-gray-300"
|
||||
@click="cancelCreate"
|
||||
>
|
||||
<i class="fas fa-times" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>分组名称 *</label
|
||||
>
|
||||
<input
|
||||
v-model="createForm.name"
|
||||
class="form-input w-full"
|
||||
placeholder="输入分组名称"
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>平台类型 *</label
|
||||
>
|
||||
<div class="flex flex-wrap gap-4">
|
||||
<label class="flex cursor-pointer items-center">
|
||||
<input v-model="createForm.platform" class="mr-2" type="radio" value="claude" />
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">Claude</span>
|
||||
</label>
|
||||
<label class="flex cursor-pointer items-center">
|
||||
<input v-model="createForm.platform" class="mr-2" type="radio" value="gemini" />
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">Gemini</span>
|
||||
</label>
|
||||
<label class="flex cursor-pointer items-center">
|
||||
<input v-model="createForm.platform" class="mr-2" type="radio" value="openai" />
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">OpenAI</span>
|
||||
</label>
|
||||
<label class="flex cursor-pointer items-center">
|
||||
<input v-model="createForm.platform" class="mr-2" type="radio" value="droid" />
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">Droid</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>描述 (可选)</label
|
||||
>
|
||||
<textarea
|
||||
v-model="createForm.description"
|
||||
class="form-input w-full resize-none"
|
||||
placeholder="分组描述..."
|
||||
rows="2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3 pt-4">
|
||||
<button
|
||||
class="btn btn-primary flex-1 px-4 py-2"
|
||||
:disabled="!createForm.name || !createForm.platform || creating"
|
||||
@click="createGroup"
|
||||
>
|
||||
<div v-if="creating" class="loading-spinner mr-2" />
|
||||
{{ creating ? '创建中...' : '创建' }}
|
||||
</button>
|
||||
<button class="btn btn-secondary flex-1 px-4 py-2" @click="cancelCreate">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 删除确认对话框 -->
|
||||
<ConfirmModal
|
||||
cancel-text="取消"
|
||||
confirm-text="确认删除"
|
||||
:message="`确定要删除分组 "${deletingGroup?.name}" 吗?此操作不可撤销。`"
|
||||
:show="showDeleteConfirm"
|
||||
title="确认删除"
|
||||
type="danger"
|
||||
@cancel="cancelDelete"
|
||||
@confirm="confirmDelete"
|
||||
/>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { showToast } from '@/utils/toast'
|
||||
import { apiClient } from '@/config/api'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { showToast } from '@/utils/tools'
|
||||
import * as httpApi from '@/utils/http_apis'
|
||||
import ConfirmModal from '@/components/common/ConfirmModal.vue'
|
||||
|
||||
const emit = defineEmits(['close', 'refresh'])
|
||||
|
||||
@@ -253,6 +313,35 @@ const show = ref(true)
|
||||
const loading = ref(false)
|
||||
const groups = ref([])
|
||||
|
||||
// Tab 切换
|
||||
const activeTab = ref('all')
|
||||
const platformTabs = [
|
||||
{ key: 'all', label: '全部', color: 'gray' },
|
||||
{ key: 'claude', label: 'Claude', color: 'purple' },
|
||||
{ key: 'gemini', label: 'Gemini', color: 'blue' },
|
||||
{ key: 'openai', label: 'OpenAI', color: 'gray' },
|
||||
{ key: 'droid', label: 'Droid', color: 'cyan' }
|
||||
]
|
||||
|
||||
// 各平台分组数量
|
||||
const platformCounts = computed(() => {
|
||||
const counts = { all: groups.value.length }
|
||||
platformTabs.slice(1).forEach((tab) => {
|
||||
counts[tab.key] = groups.value.filter((g) => g.platform === tab.key).length
|
||||
})
|
||||
return counts
|
||||
})
|
||||
|
||||
// 过滤后的分组列表
|
||||
const filteredGroups = computed(() => {
|
||||
if (activeTab.value === 'all') return groups.value
|
||||
return groups.value.filter((g) => g.platform === activeTab.value)
|
||||
})
|
||||
|
||||
// 删除确认
|
||||
const showDeleteConfirm = ref(false)
|
||||
const deletingGroup = ref(null)
|
||||
|
||||
// 创建表单
|
||||
const showCreateForm = ref(false)
|
||||
const creating = ref(false)
|
||||
@@ -283,7 +372,7 @@ const formatDate = (dateStr) => {
|
||||
const loadGroups = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await apiClient.get('/admin/account-groups')
|
||||
const response = await httpApi.get('/admin/account-groups')
|
||||
groups.value = response.data || []
|
||||
} catch (error) {
|
||||
showToast('加载分组列表失败', 'error')
|
||||
@@ -301,7 +390,7 @@ const createGroup = async () => {
|
||||
|
||||
creating.value = true
|
||||
try {
|
||||
await apiClient.post('/admin/account-groups', {
|
||||
await httpApi.post('/admin/account-groups', {
|
||||
name: createForm.value.name,
|
||||
platform: createForm.value.platform,
|
||||
description: createForm.value.description
|
||||
@@ -318,6 +407,12 @@ const createGroup = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 打开创建表单(根据当前 Tab 预选平台)
|
||||
const openCreateForm = () => {
|
||||
createForm.value.platform = activeTab.value !== 'all' ? activeTab.value : 'claude'
|
||||
showCreateForm.value = true
|
||||
}
|
||||
|
||||
// 取消创建
|
||||
const cancelCreate = () => {
|
||||
showCreateForm.value = false
|
||||
@@ -348,7 +443,7 @@ const updateGroup = async () => {
|
||||
|
||||
updating.value = true
|
||||
try {
|
||||
await apiClient.put(`/admin/account-groups/${editingGroup.value.id}`, {
|
||||
await httpApi.put(`/admin/account-groups/${editingGroup.value.id}`, {
|
||||
name: editForm.value.name,
|
||||
description: editForm.value.description
|
||||
})
|
||||
@@ -375,20 +470,23 @@ const cancelEdit = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 删除分组
|
||||
const deleteGroup = async (group) => {
|
||||
// 删除分组 - 打开确认对话框
|
||||
const deleteGroup = (group) => {
|
||||
if (group.memberCount > 0) {
|
||||
showToast('分组内还有成员,无法删除', 'error')
|
||||
return
|
||||
}
|
||||
deletingGroup.value = group
|
||||
showDeleteConfirm.value = true
|
||||
}
|
||||
|
||||
if (!confirm(`确定要删除分组 "${group.name}" 吗?`)) {
|
||||
return
|
||||
}
|
||||
|
||||
// 确认删除
|
||||
const confirmDelete = async () => {
|
||||
if (!deletingGroup.value) return
|
||||
try {
|
||||
await apiClient.delete(`/admin/account-groups/${group.id}`)
|
||||
await httpApi.del(`/admin/account-groups/${deletingGroup.value.id}`)
|
||||
showToast('分组删除成功', 'success')
|
||||
cancelDelete()
|
||||
await loadGroups()
|
||||
emit('refresh')
|
||||
} catch (error) {
|
||||
@@ -396,6 +494,12 @@ const deleteGroup = async (group) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 取消删除
|
||||
const cancelDelete = () => {
|
||||
showDeleteConfirm.value = false
|
||||
deletingGroup.value = null
|
||||
}
|
||||
|
||||
// 组件挂载时加载数据
|
||||
onMounted(() => {
|
||||
loadGroups()
|
||||
|
||||
@@ -794,7 +794,7 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, onBeforeUnmount } from 'vue'
|
||||
import { showToast } from '@/utils/toast'
|
||||
import { showToast } from '@/utils/tools'
|
||||
import { useAccountsStore } from '@/stores/accounts'
|
||||
|
||||
const props = defineProps({
|
||||
|
||||
Reference in New Issue
Block a user