mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
feat(admin-spa): 同步PR #106的会话窗口管理功能到新版管理后台
- 在账户管理表格中添加'会话窗口'列 - 显示Claude账户的会话窗口状态(进度条、时间范围、剩余时间) - 添加formatSessionWindow和formatRemainingTime格式化函数 - 完整同步原版PR #106的前端功能到重构后的admin-spa
This commit is contained in:
513
web/admin-spa/src/views/AccountsView.vue
Normal file
513
web/admin-spa/src/views/AccountsView.vue
Normal file
@@ -0,0 +1,513 @@
|
|||||||
|
<template>
|
||||||
|
<div class="accounts-container">
|
||||||
|
<div class="card p-6">
|
||||||
|
<div class="flex flex-col md:flex-row justify-between items-center gap-4 mb-6">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-xl font-bold text-gray-900 mb-2">账户管理</h3>
|
||||||
|
<p class="text-gray-600">管理您的 Claude 和 Gemini 账户及代理配置</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<select v-model="accountSortBy" @change="sortAccounts()" class="form-input px-3 py-2 text-sm">
|
||||||
|
<option value="name">按名称排序</option>
|
||||||
|
<option value="dailyTokens">按今日Token排序</option>
|
||||||
|
<option value="dailyRequests">按今日请求数排序</option>
|
||||||
|
<option value="totalTokens">按总Token排序</option>
|
||||||
|
<option value="lastUsed">按最后使用排序</option>
|
||||||
|
</select>
|
||||||
|
<button
|
||||||
|
@click.stop="openCreateAccountModal"
|
||||||
|
class="btn btn-success px-6 py-3 flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<i class="fas fa-plus"></i>添加账户
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="accountsLoading" class="text-center py-12">
|
||||||
|
<div class="loading-spinner mx-auto mb-4"></div>
|
||||||
|
<p class="text-gray-500">正在加载账户...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="sortedAccounts.length === 0" class="text-center py-12">
|
||||||
|
<div class="w-16 h-16 mx-auto mb-4 bg-gray-100 rounded-full flex items-center justify-center">
|
||||||
|
<i class="fas fa-user-circle text-gray-400 text-xl"></i>
|
||||||
|
</div>
|
||||||
|
<p class="text-gray-500 text-lg">暂无账户</p>
|
||||||
|
<p class="text-gray-400 text-sm mt-2">点击上方按钮添加您的第一个账户</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="table-container">
|
||||||
|
<table class="min-w-full">
|
||||||
|
<thead class="bg-gray-50/80 backdrop-blur-sm">
|
||||||
|
<tr>
|
||||||
|
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100" @click="sortAccounts('name')">
|
||||||
|
名称
|
||||||
|
<i v-if="accountsSortBy === 'name'" :class="['fas', accountsSortOrder === 'asc' ? 'fa-sort-up' : 'fa-sort-down', 'ml-1']"></i>
|
||||||
|
<i v-else class="fas fa-sort ml-1 text-gray-400"></i>
|
||||||
|
</th>
|
||||||
|
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100" @click="sortAccounts('platform')">
|
||||||
|
平台
|
||||||
|
<i v-if="accountsSortBy === 'platform'" :class="['fas', accountsSortOrder === 'asc' ? 'fa-sort-up' : 'fa-sort-down', 'ml-1']"></i>
|
||||||
|
<i v-else class="fas fa-sort ml-1 text-gray-400"></i>
|
||||||
|
</th>
|
||||||
|
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100" @click="sortAccounts('accountType')">
|
||||||
|
类型
|
||||||
|
<i v-if="accountsSortBy === 'accountType'" :class="['fas', accountsSortOrder === 'asc' ? 'fa-sort-up' : 'fa-sort-down', 'ml-1']"></i>
|
||||||
|
<i v-else class="fas fa-sort ml-1 text-gray-400"></i>
|
||||||
|
</th>
|
||||||
|
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100" @click="sortAccounts('status')">
|
||||||
|
状态
|
||||||
|
<i v-if="accountsSortBy === 'status'" :class="['fas', accountsSortOrder === 'asc' ? 'fa-sort-up' : 'fa-sort-down', 'ml-1']"></i>
|
||||||
|
<i v-else class="fas fa-sort ml-1 text-gray-400"></i>
|
||||||
|
</th>
|
||||||
|
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">代理</th>
|
||||||
|
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">今日使用</th>
|
||||||
|
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">会话窗口</th>
|
||||||
|
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">最后使用</th>
|
||||||
|
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-gray-200/50">
|
||||||
|
<tr v-for="account in sortedAccounts" :key="account.id" class="table-row">
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="w-8 h-8 bg-gradient-to-br from-green-500 to-green-600 rounded-lg flex items-center justify-center mr-3">
|
||||||
|
<i class="fas fa-user-circle text-white text-xs"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="text-sm font-semibold text-gray-900">{{ account.name }}</div>
|
||||||
|
<span v-if="account.accountType === 'dedicated'"
|
||||||
|
class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800">
|
||||||
|
<i class="fas fa-lock mr-1"></i>专属
|
||||||
|
</span>
|
||||||
|
<span v-else
|
||||||
|
class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
||||||
|
<i class="fas fa-share-alt mr-1"></i>共享
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-500">{{ account.id }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
|
<span v-if="account.platform === 'gemini'"
|
||||||
|
class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-yellow-100 text-yellow-800">
|
||||||
|
<i class="fas fa-robot mr-1"></i>Gemini
|
||||||
|
</span>
|
||||||
|
<span v-else
|
||||||
|
class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-indigo-100 text-indigo-800">
|
||||||
|
<i class="fas fa-brain mr-1"></i>Claude
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
|
<span v-if="account.scopes && account.scopes.length > 0"
|
||||||
|
class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-blue-100 text-blue-800">
|
||||||
|
<i class="fas fa-lock mr-1"></i>OAuth
|
||||||
|
</span>
|
||||||
|
<span v-else
|
||||||
|
class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-orange-100 text-orange-800">
|
||||||
|
<i class="fas fa-key mr-1"></i>传统
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<span :class="['inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold',
|
||||||
|
account.isActive ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800']">
|
||||||
|
<div :class="['w-2 h-2 rounded-full mr-2',
|
||||||
|
account.isActive ? 'bg-green-500' : 'bg-red-500']"></div>
|
||||||
|
{{ account.isActive ? '正常' : '异常' }}
|
||||||
|
</span>
|
||||||
|
<span v-if="account.rateLimitStatus && account.rateLimitStatus.isRateLimited"
|
||||||
|
class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-yellow-100 text-yellow-800">
|
||||||
|
<i class="fas fa-exclamation-triangle mr-1"></i>
|
||||||
|
限流中 ({{ account.rateLimitStatus.minutesRemaining }}分钟)
|
||||||
|
</span>
|
||||||
|
<span v-if="account.accountType === 'dedicated'"
|
||||||
|
class="text-xs text-gray-500">
|
||||||
|
绑定: {{ account.boundApiKeysCount || 0 }} 个API Key
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-600">
|
||||||
|
<div v-if="account.proxy" class="text-xs bg-blue-50 px-2 py-1 rounded">
|
||||||
|
{{ account.proxy.type }}://{{ account.proxy.host }}:{{ account.proxy.port }}
|
||||||
|
</div>
|
||||||
|
<div v-else class="text-gray-400">无代理</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm">
|
||||||
|
<div v-if="account.usage && account.usage.daily" class="space-y-1">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||||
|
<span class="text-sm font-medium text-gray-900">{{ account.usage.daily.requests || 0 }} 次</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="w-2 h-2 bg-blue-500 rounded-full"></div>
|
||||||
|
<span class="text-xs text-gray-600">{{ formatNumber(account.usage.daily.allTokens || 0) }} tokens</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="account.usage.averages && account.usage.averages.rpm > 0" class="text-xs text-gray-500">
|
||||||
|
平均 {{ account.usage.averages.rpm.toFixed(2) }} RPM
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="text-gray-400 text-xs">暂无数据</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
|
<div v-if="account.platform === 'claude' && account.sessionWindow && account.sessionWindow.hasActiveWindow" class="space-y-2">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="w-24 bg-gray-200 rounded-full h-2">
|
||||||
|
<div class="bg-gradient-to-r from-blue-500 to-indigo-600 h-2 rounded-full transition-all duration-300"
|
||||||
|
:style="{ width: account.sessionWindow.progress + '%' }"></div>
|
||||||
|
</div>
|
||||||
|
<span class="text-xs text-gray-700 font-medium min-w-[32px]">
|
||||||
|
{{ account.sessionWindow.progress }}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-600">
|
||||||
|
<div>{{ formatSessionWindow(account.sessionWindow.windowStart, account.sessionWindow.windowEnd) }}</div>
|
||||||
|
<div v-if="account.sessionWindow.remainingTime > 0" class="text-indigo-600 font-medium">
|
||||||
|
剩余 {{ formatRemainingTime(account.sessionWindow.remainingTime) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="account.platform === 'claude'" class="text-gray-400 text-sm">
|
||||||
|
<i class="fas fa-minus"></i>
|
||||||
|
</div>
|
||||||
|
<div v-else class="text-gray-400 text-sm">
|
||||||
|
<span class="text-xs">N/A</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-600">
|
||||||
|
{{ formatLastUsed(account.lastUsedAt) }}
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
v-if="account.platform === 'claude' && account.scopes"
|
||||||
|
@click="refreshToken(account)"
|
||||||
|
:disabled="account.isRefreshing"
|
||||||
|
:class="[
|
||||||
|
'px-3 py-1.5 rounded-lg text-xs font-medium transition-colors',
|
||||||
|
account.isRefreshing
|
||||||
|
? 'bg-gray-100 text-gray-400 cursor-not-allowed'
|
||||||
|
: 'bg-blue-100 text-blue-700 hover:bg-blue-200'
|
||||||
|
]"
|
||||||
|
:title="account.isRefreshing ? '刷新中...' : '刷新Token'"
|
||||||
|
>
|
||||||
|
<i :class="[
|
||||||
|
'fas fa-sync-alt',
|
||||||
|
account.isRefreshing ? 'animate-spin' : ''
|
||||||
|
]"></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="editAccount(account)"
|
||||||
|
class="px-3 py-1.5 bg-blue-100 text-blue-700 rounded-lg text-xs font-medium hover:bg-blue-200 transition-colors"
|
||||||
|
>
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="deleteAccount(account)"
|
||||||
|
class="px-3 py-1.5 bg-red-100 text-red-700 rounded-lg text-xs font-medium hover:bg-red-200 transition-colors"
|
||||||
|
>
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 添加账户模态框 -->
|
||||||
|
<AccountForm
|
||||||
|
v-if="showCreateAccountModal"
|
||||||
|
@close="showCreateAccountModal = false"
|
||||||
|
@success="handleCreateSuccess"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 编辑账户模态框 -->
|
||||||
|
<AccountForm
|
||||||
|
v-if="showEditAccountModal"
|
||||||
|
:account="editingAccount"
|
||||||
|
@close="showEditAccountModal = false"
|
||||||
|
@success="handleEditSuccess"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted, watch } from 'vue'
|
||||||
|
import { showToast } from '@/utils/toast'
|
||||||
|
import { apiClient } from '@/config/api'
|
||||||
|
import AccountForm from '@/components/accounts/AccountForm.vue'
|
||||||
|
|
||||||
|
// 数据状态
|
||||||
|
const accounts = ref([])
|
||||||
|
const accountsLoading = ref(false)
|
||||||
|
const accountSortBy = ref('name')
|
||||||
|
const accountsSortBy = ref('')
|
||||||
|
const accountsSortOrder = ref('asc')
|
||||||
|
const apiKeys = ref([])
|
||||||
|
|
||||||
|
// 模态框状态
|
||||||
|
const showCreateAccountModal = ref(false)
|
||||||
|
const showEditAccountModal = ref(false)
|
||||||
|
const editingAccount = ref(null)
|
||||||
|
|
||||||
|
// 计算排序后的账户列表
|
||||||
|
const sortedAccounts = computed(() => {
|
||||||
|
if (!accountsSortBy.value) return accounts.value
|
||||||
|
|
||||||
|
const sorted = [...accounts.value].sort((a, b) => {
|
||||||
|
let aVal = a[accountsSortBy.value]
|
||||||
|
let bVal = b[accountsSortBy.value]
|
||||||
|
|
||||||
|
// 处理统计数据
|
||||||
|
if (accountsSortBy.value === 'dailyTokens') {
|
||||||
|
aVal = a.usage?.daily?.allTokens || 0
|
||||||
|
bVal = b.usage?.daily?.allTokens || 0
|
||||||
|
} else if (accountsSortBy.value === 'dailyRequests') {
|
||||||
|
aVal = a.usage?.daily?.requests || 0
|
||||||
|
bVal = b.usage?.daily?.requests || 0
|
||||||
|
} else if (accountsSortBy.value === 'totalTokens') {
|
||||||
|
aVal = a.usage?.total?.allTokens || 0
|
||||||
|
bVal = b.usage?.total?.allTokens || 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理最后使用时间
|
||||||
|
if (accountsSortBy.value === 'lastUsed') {
|
||||||
|
aVal = a.lastUsedAt ? new Date(a.lastUsedAt).getTime() : 0
|
||||||
|
bVal = b.lastUsedAt ? new Date(b.lastUsedAt).getTime() : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理状态
|
||||||
|
if (accountsSortBy.value === 'status') {
|
||||||
|
aVal = a.isActive ? 1 : 0
|
||||||
|
bVal = b.isActive ? 1 : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (aVal < bVal) return accountsSortOrder.value === 'asc' ? -1 : 1
|
||||||
|
if (aVal > bVal) return accountsSortOrder.value === 'asc' ? 1 : -1
|
||||||
|
return 0
|
||||||
|
})
|
||||||
|
|
||||||
|
return sorted
|
||||||
|
})
|
||||||
|
|
||||||
|
// 加载账户列表
|
||||||
|
const loadAccounts = async () => {
|
||||||
|
accountsLoading.value = true
|
||||||
|
try {
|
||||||
|
const [claudeData, geminiData, apiKeysData] = await Promise.all([
|
||||||
|
apiClient.get('/admin/claude-accounts'),
|
||||||
|
apiClient.get('/admin/gemini-accounts'),
|
||||||
|
apiClient.get('/admin/api-keys')
|
||||||
|
])
|
||||||
|
|
||||||
|
// 更新API Keys列表
|
||||||
|
if (apiKeysData.success) {
|
||||||
|
apiKeys.value = apiKeysData.data || []
|
||||||
|
}
|
||||||
|
|
||||||
|
const allAccounts = []
|
||||||
|
|
||||||
|
if (claudeData.success) {
|
||||||
|
const claudeAccounts = (claudeData.data || []).map(acc => {
|
||||||
|
// 计算每个Claude账户绑定的API Key数量
|
||||||
|
const boundApiKeysCount = apiKeys.value.filter(key => key.claudeAccountId === acc.id).length
|
||||||
|
return { ...acc, platform: 'claude', boundApiKeysCount }
|
||||||
|
})
|
||||||
|
allAccounts.push(...claudeAccounts)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (geminiData.success) {
|
||||||
|
const geminiAccounts = (geminiData.data || []).map(acc => {
|
||||||
|
// 计算每个Gemini账户绑定的API Key数量
|
||||||
|
const boundApiKeysCount = apiKeys.value.filter(key => key.geminiAccountId === acc.id).length
|
||||||
|
return { ...acc, platform: 'gemini', boundApiKeysCount }
|
||||||
|
})
|
||||||
|
allAccounts.push(...geminiAccounts)
|
||||||
|
}
|
||||||
|
|
||||||
|
accounts.value = allAccounts
|
||||||
|
} catch (error) {
|
||||||
|
showToast('加载账户失败', 'error')
|
||||||
|
} finally {
|
||||||
|
accountsLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 排序账户
|
||||||
|
const sortAccounts = (field) => {
|
||||||
|
if (field) {
|
||||||
|
if (accountsSortBy.value === field) {
|
||||||
|
accountsSortOrder.value = accountsSortOrder.value === 'asc' ? 'desc' : 'asc'
|
||||||
|
} else {
|
||||||
|
accountsSortBy.value = field
|
||||||
|
accountsSortOrder.value = 'asc'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化数字(与原版保持一致)
|
||||||
|
const formatNumber = (num) => {
|
||||||
|
if (num === null || num === undefined) return '0'
|
||||||
|
const number = Number(num)
|
||||||
|
if (number >= 1000000) {
|
||||||
|
return Math.floor(number / 1000000).toLocaleString() + 'M'
|
||||||
|
}
|
||||||
|
return number.toLocaleString()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化最后使用时间
|
||||||
|
const formatLastUsed = (dateString) => {
|
||||||
|
if (!dateString) return '从未使用'
|
||||||
|
|
||||||
|
const date = new Date(dateString)
|
||||||
|
const now = new Date()
|
||||||
|
const diff = now - date
|
||||||
|
|
||||||
|
if (diff < 60000) return '刚刚'
|
||||||
|
if (diff < 3600000) return `${Math.floor(diff / 60000)} 分钟前`
|
||||||
|
if (diff < 86400000) return `${Math.floor(diff / 3600000)} 小时前`
|
||||||
|
if (diff < 604800000) return `${Math.floor(diff / 86400000)} 天前`
|
||||||
|
|
||||||
|
return date.toLocaleDateString('zh-CN')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化会话窗口时间
|
||||||
|
const formatSessionWindow = (windowStart, windowEnd) => {
|
||||||
|
if (!windowStart || !windowEnd) return '--'
|
||||||
|
|
||||||
|
const start = new Date(windowStart)
|
||||||
|
const end = new Date(windowEnd)
|
||||||
|
|
||||||
|
const startHour = start.getHours().toString().padStart(2, '0')
|
||||||
|
const startMin = start.getMinutes().toString().padStart(2, '0')
|
||||||
|
const endHour = end.getHours().toString().padStart(2, '0')
|
||||||
|
const endMin = end.getMinutes().toString().padStart(2, '0')
|
||||||
|
|
||||||
|
return `${startHour}:${startMin} - ${endHour}:${endMin}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化剩余时间
|
||||||
|
const formatRemainingTime = (minutes) => {
|
||||||
|
if (!minutes || minutes <= 0) return '已结束'
|
||||||
|
|
||||||
|
const hours = Math.floor(minutes / 60)
|
||||||
|
const mins = minutes % 60
|
||||||
|
|
||||||
|
if (hours > 0) {
|
||||||
|
return `${hours}小时${mins}分钟`
|
||||||
|
}
|
||||||
|
return `${mins}分钟`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开创建账户模态框
|
||||||
|
const openCreateAccountModal = () => {
|
||||||
|
showCreateAccountModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑账户
|
||||||
|
const editAccount = (account) => {
|
||||||
|
editingAccount.value = account
|
||||||
|
showEditAccountModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 删除账户
|
||||||
|
const deleteAccount = async (account) => {
|
||||||
|
if (!confirm(`确定要删除账户 "${account.name}" 吗?此操作不可恢复。`)) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const endpoint = account.platform === 'claude'
|
||||||
|
? `/admin/claude-accounts/${account.id}`
|
||||||
|
: `/admin/gemini-accounts/${account.id}`
|
||||||
|
|
||||||
|
const data = await apiClient.delete(endpoint)
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
showToast('账户已删除', 'success')
|
||||||
|
loadAccounts()
|
||||||
|
} else {
|
||||||
|
showToast(data.message || '删除失败', 'error')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showToast('删除失败', 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新Token
|
||||||
|
const refreshToken = async (account) => {
|
||||||
|
if (account.isRefreshing) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
account.isRefreshing = true
|
||||||
|
const data = await apiClient.post(`/admin/claude-accounts/${account.id}/refresh`)
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
showToast('Token刷新成功', 'success')
|
||||||
|
loadAccounts()
|
||||||
|
} else {
|
||||||
|
showToast(data.message || 'Token刷新失败', 'error')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showToast('Token刷新失败', 'error')
|
||||||
|
} finally {
|
||||||
|
account.isRefreshing = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理创建成功
|
||||||
|
const handleCreateSuccess = () => {
|
||||||
|
showCreateAccountModal.value = false
|
||||||
|
showToast('账户创建成功', 'success')
|
||||||
|
loadAccounts()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理编辑成功
|
||||||
|
const handleEditSuccess = () => {
|
||||||
|
showEditAccountModal.value = false
|
||||||
|
showToast('账户更新成功', 'success')
|
||||||
|
loadAccounts()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听排序选择变化
|
||||||
|
watch(accountSortBy, (newVal) => {
|
||||||
|
const fieldMap = {
|
||||||
|
name: 'name',
|
||||||
|
dailyTokens: 'dailyTokens',
|
||||||
|
dailyRequests: 'dailyRequests',
|
||||||
|
totalTokens: 'totalTokens',
|
||||||
|
lastUsed: 'lastUsed'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fieldMap[newVal]) {
|
||||||
|
sortAccounts(fieldMap[newVal])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadAccounts()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.accounts-container {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-row:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.02);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user