mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 00:53:33 +00:00
Merge branch 'main' into new
This commit is contained in:
@@ -304,7 +304,25 @@ const selectQuickOption = (value) => {
|
||||
// 更新自定义过期时间
|
||||
const updateCustomExpiryPreview = () => {
|
||||
if (localForm.customExpireDate) {
|
||||
localForm.expiresAt = new Date(localForm.customExpireDate).toISOString()
|
||||
try {
|
||||
// 手动解析日期时间字符串,确保它被正确解释为本地时间
|
||||
const [datePart, timePart] = localForm.customExpireDate.split('T')
|
||||
const [year, month, day] = datePart.split('-').map(Number)
|
||||
const [hours, minutes] = timePart.split(':').map(Number)
|
||||
|
||||
// 使用构造函数创建本地时间的 Date 对象,然后转换为 UTC ISO 字符串
|
||||
const localDate = new Date(year, month - 1, day, hours, minutes, 0, 0)
|
||||
|
||||
// 验证日期有效性
|
||||
if (isNaN(localDate.getTime())) {
|
||||
console.error('Invalid date:', localForm.customExpireDate)
|
||||
return
|
||||
}
|
||||
|
||||
localForm.expiresAt = localDate.toISOString()
|
||||
} catch (error) {
|
||||
console.error('Failed to parse custom expire date:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4841,11 +4841,23 @@ const handleGroupRefresh = async () => {
|
||||
// 处理 API Key 管理模态框刷新
|
||||
const handleApiKeyRefresh = async () => {
|
||||
// 刷新账户信息以更新 API Key 数量
|
||||
if (props.account?.id) {
|
||||
if (!props.account?.id) {
|
||||
return
|
||||
}
|
||||
|
||||
const refreshers = [
|
||||
typeof accountsStore.fetchDroidAccounts === 'function'
|
||||
? accountsStore.fetchDroidAccounts
|
||||
: null,
|
||||
typeof accountsStore.fetchAllAccounts === 'function' ? accountsStore.fetchAllAccounts : null
|
||||
].filter(Boolean)
|
||||
|
||||
for (const refresher of refreshers) {
|
||||
try {
|
||||
await accountsStore.fetchAccounts()
|
||||
await refresher()
|
||||
return
|
||||
} catch (error) {
|
||||
console.error('Failed to refresh account data:', error)
|
||||
console.error('刷新账户列表失败:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,12 +20,28 @@
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="p-1 text-gray-400 transition-colors hover:text-gray-600 dark:hover:text-gray-300"
|
||||
@click="$emit('close')"
|
||||
>
|
||||
<i class="fas fa-times text-lg sm:text-xl" />
|
||||
</button>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
class="flex items-center gap-2 rounded-lg border border-purple-200 bg-white/90 px-3 py-1.5 text-xs font-semibold text-purple-600 shadow-sm transition-all duration-200 hover:border-purple-300 hover:bg-purple-50 hover:text-purple-700 focus:outline-none focus:ring-2 focus:ring-purple-200 disabled:cursor-not-allowed disabled:opacity-60 dark:border-purple-600/60 dark:bg-purple-900/20 dark:text-purple-200 dark:hover:border-purple-500 dark:hover:bg-purple-900/40 dark:hover:text-purple-100 dark:focus:ring-purple-500/40 sm:text-sm"
|
||||
:disabled="loading || apiKeys.length === 0 || copyingAll"
|
||||
@click="copyAllApiKeys"
|
||||
>
|
||||
<i
|
||||
:class="[
|
||||
'text-sm sm:text-base',
|
||||
copyingAll ? 'fas fa-spinner fa-spin' : 'fas fa-clipboard-list'
|
||||
]"
|
||||
/>
|
||||
<span>复制全部 Key</span>
|
||||
</button>
|
||||
<button
|
||||
class="flex h-9 w-9 items-center justify-center rounded-full bg-gray-100 text-gray-400 transition-colors hover:text-gray-600 dark:bg-gray-800 dark:text-gray-400 dark:hover:text-gray-200 sm:h-10 sm:w-10"
|
||||
title="关闭"
|
||||
@click="$emit('close')"
|
||||
>
|
||||
<i class="fas fa-times text-base sm:text-lg" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
@@ -393,6 +409,7 @@ const resetting = ref(null)
|
||||
const apiKeys = ref([])
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(15)
|
||||
const copyingAll = ref(false)
|
||||
|
||||
// 新增:筛选和搜索相关状态
|
||||
const statusFilter = ref('all') // 'all' | 'active' | 'error'
|
||||
@@ -703,14 +720,71 @@ const exportKeys = (type) => {
|
||||
showToast(`成功导出 ${keysToExport.length} 个 API Key`, 'success')
|
||||
}
|
||||
|
||||
// 写入剪贴板(带回退逻辑)
|
||||
const writeToClipboard = async (text) => {
|
||||
const canUseClipboardApi =
|
||||
typeof navigator !== 'undefined' &&
|
||||
navigator.clipboard &&
|
||||
typeof navigator.clipboard.writeText === 'function' &&
|
||||
(typeof window === 'undefined' || window.isSecureContext !== false)
|
||||
|
||||
if (canUseClipboardApi) {
|
||||
await navigator.clipboard.writeText(text)
|
||||
return
|
||||
}
|
||||
|
||||
if (typeof document === 'undefined') {
|
||||
throw new Error('clipboard unavailable')
|
||||
}
|
||||
|
||||
const textarea = document.createElement('textarea')
|
||||
textarea.value = text
|
||||
textarea.setAttribute('readonly', '')
|
||||
textarea.style.position = 'fixed'
|
||||
textarea.style.opacity = '0'
|
||||
textarea.style.pointerEvents = 'none'
|
||||
document.body.appendChild(textarea)
|
||||
textarea.select()
|
||||
|
||||
try {
|
||||
const success = document.execCommand('copy')
|
||||
document.body.removeChild(textarea)
|
||||
if (!success) {
|
||||
throw new Error('execCommand failed')
|
||||
}
|
||||
} catch (error) {
|
||||
document.body.removeChild(textarea)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 复制 API Key
|
||||
const copyApiKey = async (key) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(key)
|
||||
await writeToClipboard(key)
|
||||
showToast('API Key 已复制', 'success')
|
||||
} catch (error) {
|
||||
console.error('Failed to copy:', error)
|
||||
showToast('复制失败', 'error')
|
||||
showToast('复制失败,请手动复制', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
// 复制全部 API Key
|
||||
const copyAllApiKeys = async () => {
|
||||
if (!apiKeys.value.length || copyingAll.value) {
|
||||
return
|
||||
}
|
||||
|
||||
copyingAll.value = true
|
||||
try {
|
||||
const allKeysText = apiKeys.value.map((item) => item.key).join('\n')
|
||||
await writeToClipboard(allKeysText)
|
||||
showToast(`已复制 ${apiKeys.value.length} 条 API Key`, 'success')
|
||||
} catch (error) {
|
||||
console.error('Failed to copy all keys:', error)
|
||||
showToast('复制全部 API Key 失败,请手动复制', 'error')
|
||||
} finally {
|
||||
copyingAll.value = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3728,47 +3728,54 @@ const closeAccountExpiryEdit = () => {
|
||||
editingExpiryAccount.value = null
|
||||
}
|
||||
|
||||
// 根据账户平台解析更新端点
|
||||
const resolveAccountUpdateEndpoint = (account) => {
|
||||
switch (account.platform) {
|
||||
case 'claude':
|
||||
return `/admin/claude-accounts/${account.id}`
|
||||
case 'claude-console':
|
||||
return `/admin/claude-console-accounts/${account.id}`
|
||||
case 'bedrock':
|
||||
return `/admin/bedrock-accounts/${account.id}`
|
||||
case 'openai':
|
||||
return `/admin/openai-accounts/${account.id}`
|
||||
case 'azure_openai':
|
||||
return `/admin/azure-openai-accounts/${account.id}`
|
||||
case 'openai-responses':
|
||||
return `/admin/openai-responses-accounts/${account.id}`
|
||||
case 'ccr':
|
||||
return `/admin/ccr-accounts/${account.id}`
|
||||
case 'gemini':
|
||||
return `/admin/gemini-accounts/${account.id}`
|
||||
case 'droid':
|
||||
return `/admin/droid-accounts/${account.id}`
|
||||
default:
|
||||
throw new Error(`Unsupported platform: ${account.platform}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存账户过期时间
|
||||
const handleSaveAccountExpiry = async ({ accountId, expiresAt }) => {
|
||||
try {
|
||||
// 找到对应的账户以获取平台信息
|
||||
// 根据账号平台选择正确的 API 端点
|
||||
const account = accounts.value.find((acc) => acc.id === accountId)
|
||||
|
||||
if (!account) {
|
||||
showToast('账户不存在', 'error')
|
||||
if (expiryEditModalRef.value) {
|
||||
expiryEditModalRef.value.resetSaving()
|
||||
}
|
||||
showToast('未找到账户', 'error')
|
||||
return
|
||||
}
|
||||
|
||||
// 根据平台动态选择端点
|
||||
const endpoint = resolveAccountUpdateEndpoint(account)
|
||||
// 定义每个平台的端点和参数名
|
||||
// 注意:部分平台使用 :accountId,部分使用 :id
|
||||
let endpoint = ''
|
||||
switch (account.platform) {
|
||||
case 'claude':
|
||||
case 'claude-oauth':
|
||||
endpoint = `/admin/claude-accounts/${accountId}`
|
||||
break
|
||||
case 'gemini':
|
||||
endpoint = `/admin/gemini-accounts/${accountId}`
|
||||
break
|
||||
case 'claude-console':
|
||||
endpoint = `/admin/claude-console-accounts/${accountId}`
|
||||
break
|
||||
case 'bedrock':
|
||||
endpoint = `/admin/bedrock-accounts/${accountId}`
|
||||
break
|
||||
case 'ccr':
|
||||
endpoint = `/admin/ccr-accounts/${accountId}`
|
||||
break
|
||||
case 'openai':
|
||||
endpoint = `/admin/openai-accounts/${accountId}` // 使用 :id
|
||||
break
|
||||
case 'droid':
|
||||
endpoint = `/admin/droid-accounts/${accountId}` // 使用 :id
|
||||
break
|
||||
case 'azure_openai':
|
||||
endpoint = `/admin/azure-openai-accounts/${accountId}` // 使用 :id
|
||||
break
|
||||
case 'openai-responses':
|
||||
endpoint = `/admin/openai-responses-accounts/${accountId}` // 使用 :id
|
||||
break
|
||||
default:
|
||||
showToast(`不支持的平台类型: ${account.platform}`, 'error')
|
||||
return
|
||||
}
|
||||
|
||||
const data = await apiClient.put(endpoint, {
|
||||
expiresAt: expiresAt || null
|
||||
})
|
||||
@@ -3786,7 +3793,8 @@ const handleSaveAccountExpiry = async ({ accountId, expiresAt }) => {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
showToast(error.message || '更新失败', 'error')
|
||||
console.error('更新账户过期时间失败:', error)
|
||||
showToast('更新失败', 'error')
|
||||
// 重置保存状态
|
||||
if (expiryEditModalRef.value) {
|
||||
expiryEditModalRef.value.resetSaving()
|
||||
@@ -3802,6 +3810,7 @@ onMounted(() => {
|
||||
|
||||
<style scoped>
|
||||
.table-container {
|
||||
overflow-x: auto;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
@@ -3835,6 +3844,12 @@ onMounted(() => {
|
||||
min-height: calc(100vh - 300px);
|
||||
}
|
||||
|
||||
.table-container {
|
||||
overflow-x: auto;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.table-row {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user