Merge branch 'main' into new

This commit is contained in:
AAEE86
2025-10-15 23:38:18 +08:00
committed by GitHub
29 changed files with 1475 additions and 675 deletions

View File

@@ -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)
}
}
}

View File

@@ -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)
}
}
}

View File

@@ -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
}
}